@yemi33/minions 0.1.1813 → 0.1.1815
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 +10 -0
- package/bin/minions.js +17 -1
- package/engine/copilot-models.json +1 -1
- package/engine/pipeline.js +4 -5
- package/engine/playbook.js +23 -3
- package/engine/projects.js +92 -15
- package/engine/restart-health.js +169 -0
- package/engine/shared.js +2 -1
- package/engine.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/bin/minions.js
CHANGED
|
@@ -40,6 +40,7 @@ const { spawn, spawnSync, execSync } = require('child_process');
|
|
|
40
40
|
|
|
41
41
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
42
42
|
const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
|
|
43
|
+
const { waitForRestartHealth, formatRestartHealthError } = require(path.join(PKG_ROOT, 'engine', 'restart-health'));
|
|
43
44
|
const DASH_PORT = 7331;
|
|
44
45
|
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
45
46
|
|
|
@@ -708,7 +709,22 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
708
709
|
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
|
709
710
|
const dashProc = spawnDashboard(suppressDashboardOpen);
|
|
710
711
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
711
|
-
console.log(` Dashboard: http://localhost:${DASH_PORT}
|
|
712
|
+
console.log(` Dashboard: http://localhost:${DASH_PORT}`);
|
|
713
|
+
console.log(' Verifying restart health...');
|
|
714
|
+
void (async () => {
|
|
715
|
+
const result = await waitForRestartHealth({
|
|
716
|
+
minionsHome: MINIONS_HOME,
|
|
717
|
+
dashboardUrl: `http://127.0.0.1:${DASH_PORT}/api/health`,
|
|
718
|
+
});
|
|
719
|
+
if (!result.ok) {
|
|
720
|
+
console.error(formatRestartHealthError(result));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.\n`);
|
|
724
|
+
})().catch(err => {
|
|
725
|
+
console.error(`\n ERROR: Restart verification failed: ${err.message}\n`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
});
|
|
712
728
|
} else if (cmd === 'nuke') {
|
|
713
729
|
ensureInstalled();
|
|
714
730
|
if (!rest.includes('--confirm')) {
|
package/engine/pipeline.js
CHANGED
|
@@ -390,11 +390,10 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
390
390
|
touchedProjects.push(project?.name || null);
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
};
|
|
393
|
+
const artifacts = { workItems: [...new Set(createdIds)] };
|
|
394
|
+
const projectArtifacts = [...new Set(touchedProjects.filter(Boolean))];
|
|
395
|
+
if (projectArtifacts.length > 0) artifacts.projects = projectArtifacts;
|
|
396
|
+
return { status: PIPELINE_STATUS.RUNNING, artifacts };
|
|
398
397
|
}
|
|
399
398
|
|
|
400
399
|
function executeTaskStageLegacy(stage, stageState, run, config) {
|
package/engine/playbook.js
CHANGED
|
@@ -342,21 +342,41 @@ function isSafeProjectPlaybookName(projectName) {
|
|
|
342
342
|
!/^[a-zA-Z]:/.test(name);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
function isSafePlaybookType(playbookType) {
|
|
346
|
+
const name = String(playbookType || '').trim();
|
|
347
|
+
return !!name &&
|
|
348
|
+
!name.includes('\0') &&
|
|
349
|
+
!name.includes('..') &&
|
|
350
|
+
!/[\\/]/.test(name) &&
|
|
351
|
+
!path.isAbsolute(name) &&
|
|
352
|
+
!/^[a-zA-Z]:/.test(name);
|
|
353
|
+
}
|
|
354
|
+
|
|
345
355
|
function resolvePlaybookPath(projectName, playbookType) {
|
|
356
|
+
const playbookTypeName = String(playbookType || '').trim();
|
|
357
|
+
if (!isSafePlaybookType(playbookTypeName)) {
|
|
358
|
+
log('warn', `Skipping unsafe playbook type lookup: ${playbookTypeName.replace(/\0/g, '')}`);
|
|
359
|
+
return path.join(PLAYBOOKS_DIR, '__invalid__.md');
|
|
360
|
+
}
|
|
346
361
|
if (projectName) {
|
|
347
362
|
const projectDirName = String(projectName).trim();
|
|
348
363
|
if (isSafeProjectPlaybookName(projectDirName)) {
|
|
349
|
-
const
|
|
364
|
+
const projectsDir = path.join(MINIONS_DIR, 'projects');
|
|
365
|
+
const localPath = path.resolve(projectsDir, projectDirName, 'playbooks', `${playbookTypeName}.md`);
|
|
366
|
+
if (!shared.isPathInside(localPath, projectsDir)) {
|
|
367
|
+
log('warn', `Skipping project-local playbook outside projects/: ${projectDirName}`);
|
|
368
|
+
return path.join(PLAYBOOKS_DIR, `${playbookTypeName}.md`);
|
|
369
|
+
}
|
|
350
370
|
try {
|
|
351
371
|
fs.accessSync(localPath, fs.constants.R_OK);
|
|
352
|
-
log('info', `Using project-local playbook: projects/${projectDirName}/playbooks/${
|
|
372
|
+
log('info', `Using project-local playbook: projects/${projectDirName}/playbooks/${playbookTypeName}.md`);
|
|
353
373
|
return localPath;
|
|
354
374
|
} catch { /* no local override — fall through to global */ }
|
|
355
375
|
} else {
|
|
356
376
|
log('warn', `Skipping project-local playbook lookup for unsafe project name: ${projectDirName}`);
|
|
357
377
|
}
|
|
358
378
|
}
|
|
359
|
-
return path.join(PLAYBOOKS_DIR, `${
|
|
379
|
+
return path.join(PLAYBOOKS_DIR, `${playbookTypeName}.md`);
|
|
360
380
|
}
|
|
361
381
|
|
|
362
382
|
|
package/engine/projects.js
CHANGED
|
@@ -11,13 +11,84 @@ const shared = require('./shared');
|
|
|
11
11
|
const dispatch = require('./dispatch');
|
|
12
12
|
const { MINIONS_DIR } = shared;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
function _sameProjectName(a, b) {
|
|
15
|
+
return String(a || '').toLowerCase() === String(b || '').toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _projectRefMatches(projectRef, removedProject, projects) {
|
|
19
|
+
if (!projectRef) return false;
|
|
20
|
+
const refs = (projectRef && typeof projectRef === 'object')
|
|
21
|
+
? [projectRef.name, projectRef.localPath]
|
|
22
|
+
: [projectRef];
|
|
23
|
+
return refs.some(ref => {
|
|
24
|
+
if (!ref) return false;
|
|
25
|
+
const result = shared.resolveConfiguredProject(ref, projects);
|
|
26
|
+
return !!(result.project && _sameProjectName(result.project.name, removedProject.name));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _workItemMatchesProject(item, removedProject, projects) {
|
|
31
|
+
const refs = [
|
|
32
|
+
item?.project,
|
|
33
|
+
item?._project,
|
|
34
|
+
item?.planProject,
|
|
35
|
+
item?._planProject,
|
|
36
|
+
item?._declaredPlanProject,
|
|
37
|
+
];
|
|
38
|
+
return refs.some(ref => _projectRefMatches(ref, removedProject, projects));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _isCentralDispatchSource(source) {
|
|
42
|
+
return source === 'central-work-item' || source === 'central-work-item-fanout';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _dispatchMatchesProject(d, removedProject, projects) {
|
|
46
|
+
const meta = d?.meta || {};
|
|
47
|
+
if (_isCentralDispatchSource(meta.source)) {
|
|
48
|
+
return _workItemMatchesProject(meta.item, removedProject, projects) ||
|
|
49
|
+
_projectRefMatches(meta.project, removedProject, projects);
|
|
50
|
+
}
|
|
51
|
+
return _workItemMatchesProject(meta.item, removedProject, projects) ||
|
|
52
|
+
_projectRefMatches(meta.project, removedProject, projects);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _centralDispatchDefaultedToProject(d, removedProject, projects) {
|
|
56
|
+
const meta = d?.meta || {};
|
|
57
|
+
return _isCentralDispatchSource(meta.source) &&
|
|
58
|
+
meta.item?.id &&
|
|
59
|
+
!_workItemMatchesProject(meta.item, removedProject, projects) &&
|
|
60
|
+
_projectRefMatches(meta.project, removedProject, projects);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _collectProjectlessCentralDispatchItemIds(removedProject, projects) {
|
|
64
|
+
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
65
|
+
const ids = new Set();
|
|
66
|
+
const state = shared.safeJson(dispatchPath) || {};
|
|
67
|
+
for (const queue of ['pending', 'active']) {
|
|
68
|
+
for (const d of Array.isArray(state?.[queue]) ? state[queue] : []) {
|
|
69
|
+
if (_centralDispatchDefaultedToProject(d, removedProject, projects)) ids.add(d.meta.item.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return ids;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _requeueProjectlessCentralWorkItems(itemIds) {
|
|
76
|
+
if (!itemIds || itemIds.size === 0) return 0;
|
|
77
|
+
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
78
|
+
if (!fs.existsSync(wiPath)) return 0;
|
|
79
|
+
let requeued = 0;
|
|
80
|
+
shared.mutateWorkItems(wiPath, items => {
|
|
81
|
+
if (!Array.isArray(items)) return items;
|
|
82
|
+
for (const w of items) {
|
|
83
|
+
if (!itemIds.has(w?.id) || w.status !== shared.WI_STATUS.DISPATCHED) continue;
|
|
84
|
+
w.status = shared.WI_STATUS.PENDING;
|
|
85
|
+
delete w.dispatched_at;
|
|
86
|
+
delete w.dispatched_to;
|
|
87
|
+
requeued++;
|
|
88
|
+
}
|
|
89
|
+
return items;
|
|
90
|
+
});
|
|
91
|
+
return requeued;
|
|
21
92
|
}
|
|
22
93
|
|
|
23
94
|
/**
|
|
@@ -56,9 +127,10 @@ function removeProject(target, options = {}) {
|
|
|
56
127
|
try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); }
|
|
57
128
|
catch (e) { return { ...summary, error: 'Failed to read config: ' + e.message }; }
|
|
58
129
|
|
|
59
|
-
const
|
|
130
|
+
const projects = shared.getProjects(config);
|
|
131
|
+
const project = shared.resolveConfiguredProject(target, projects).project;
|
|
60
132
|
if (!project) {
|
|
61
|
-
const available =
|
|
133
|
+
const available = projects.map(p => p.name).join(', ') || '(none)';
|
|
62
134
|
return { ...summary, error: `No project linked matching: ${target}. Available: ${available}` };
|
|
63
135
|
}
|
|
64
136
|
summary.project = { name: project.name, localPath: project.localPath };
|
|
@@ -72,15 +144,17 @@ function removeProject(target, options = {}) {
|
|
|
72
144
|
);
|
|
73
145
|
summary.cancelledItems += dispatch.cancelPendingWorkItems(
|
|
74
146
|
path.join(MINIONS_DIR, 'work-items.json'),
|
|
75
|
-
w =>
|
|
147
|
+
w => _workItemMatchesProject(w, project, projects),
|
|
76
148
|
'project-removed',
|
|
77
149
|
);
|
|
78
150
|
|
|
79
151
|
// 2. Drain dispatch — also kills active agent processes and unlinks pid +
|
|
80
152
|
// prompt sidecars in engine/tmp/, matching what plan delete does.
|
|
153
|
+
const projectlessCentralItemIds = _collectProjectlessCentralDispatchItemIds(project, projects);
|
|
81
154
|
summary.drainedDispatches = dispatch.cleanDispatchEntries(
|
|
82
|
-
d =>
|
|
155
|
+
d => _dispatchMatchesProject(d, project, projects),
|
|
83
156
|
);
|
|
157
|
+
_requeueProjectlessCentralWorkItems(projectlessCentralItemIds);
|
|
84
158
|
|
|
85
159
|
// 3. Clean up worktrees under this project's worktree root, honoring
|
|
86
160
|
// config.engine.worktreeRoot (mirrors lifecycle.js cleanupPlanWorktrees).
|
|
@@ -103,7 +177,7 @@ function removeProject(target, options = {}) {
|
|
|
103
177
|
// specifically. Don't touch schedules with project='any' or unset.
|
|
104
178
|
if (Array.isArray(config.schedules)) {
|
|
105
179
|
for (const s of config.schedules) {
|
|
106
|
-
if (
|
|
180
|
+
if (_projectRefMatches(s.project, project, projects) && s.enabled !== false) {
|
|
107
181
|
s.enabled = false;
|
|
108
182
|
summary.disabledSchedules++;
|
|
109
183
|
}
|
|
@@ -129,7 +203,7 @@ function removeProject(target, options = {}) {
|
|
|
129
203
|
for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
|
|
130
204
|
try {
|
|
131
205
|
const prd = JSON.parse(fs.readFileSync(path.join(prdDir, f), 'utf8'));
|
|
132
|
-
if (prd?.project
|
|
206
|
+
if (!_projectRefMatches(prd?.project, project, projects)) continue;
|
|
133
207
|
archivePlan(f, prd, [project], config);
|
|
134
208
|
summary.archivedPlans.push('prd/' + f);
|
|
135
209
|
if (prd.source_plan) archivedSourcePlans.add(prd.source_plan);
|
|
@@ -164,14 +238,17 @@ function removeProject(target, options = {}) {
|
|
|
164
238
|
...(p.monitoredResources || []),
|
|
165
239
|
...((p.stages || []).flatMap(s => s.monitoredResources || [])),
|
|
166
240
|
];
|
|
167
|
-
if (refs.some(r => r &&
|
|
241
|
+
if (refs.some(r => r && (
|
|
242
|
+
_projectRefMatches(r.project, project, projects) ||
|
|
243
|
+
_projectRefMatches(r._project, project, projects)
|
|
244
|
+
))) {
|
|
168
245
|
summary.pipelineRefs.push(p.id);
|
|
169
246
|
}
|
|
170
247
|
}
|
|
171
248
|
} catch { /* pipelines optional */ }
|
|
172
249
|
|
|
173
250
|
// 7. Remove from config.json (and persist any schedule disables)
|
|
174
|
-
config.projects = (config.projects || []).filter(p => !
|
|
251
|
+
config.projects = (config.projects || []).filter(p => !_sameProjectName(p?.name, project.name));
|
|
175
252
|
try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); }
|
|
176
253
|
catch (e) { return { ...summary, error: 'Failed to write config: ' + e.message }; }
|
|
177
254
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_RESTART_HEALTH_TIMEOUT_MS = 15000;
|
|
8
|
+
const DEFAULT_RESTART_HEALTH_INTERVAL_MS = 250;
|
|
9
|
+
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readEngineControl(minionsHome) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(path.join(minionsHome, 'engine', 'control.json'), 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizePid(pid) {
|
|
23
|
+
const n = Number(pid);
|
|
24
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isProcessAlive(pid) {
|
|
28
|
+
const n = normalizePid(pid);
|
|
29
|
+
if (!n || n === process.pid) return false;
|
|
30
|
+
try {
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
const out = execSync(`tasklist /FI "PID eq ${n}" /NH`, {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
timeout: 3000,
|
|
36
|
+
});
|
|
37
|
+
return new RegExp(`\\b${n}\\b`).test(out) && out.toLowerCase().includes('node');
|
|
38
|
+
}
|
|
39
|
+
process.kill(n, 0);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function httpGetJson(url, timeoutMs = 1000) {
|
|
47
|
+
return new Promise(resolve => {
|
|
48
|
+
let settled = false;
|
|
49
|
+
const parsed = new URL(url);
|
|
50
|
+
const client = parsed.protocol === 'https:' ? https : http;
|
|
51
|
+
const req = client.get(parsed, { timeout: timeoutMs }, res => {
|
|
52
|
+
let body = '';
|
|
53
|
+
res.setEncoding('utf8');
|
|
54
|
+
res.on('data', chunk => { body += chunk; });
|
|
55
|
+
res.on('end', () => {
|
|
56
|
+
if (settled) return;
|
|
57
|
+
settled = true;
|
|
58
|
+
let json = null;
|
|
59
|
+
try { json = body ? JSON.parse(body) : null; }
|
|
60
|
+
catch (err) {
|
|
61
|
+
resolve({ ok: false, statusCode: res.statusCode, error: err, body });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, json, body });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
req.on('timeout', () => {
|
|
68
|
+
if (settled) return;
|
|
69
|
+
settled = true;
|
|
70
|
+
req.destroy();
|
|
71
|
+
resolve({ ok: false, error: new Error(`timed out after ${timeoutMs}ms`) });
|
|
72
|
+
});
|
|
73
|
+
req.on('error', err => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
resolve({ ok: false, error: err });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function checkRestartHealth(options = {}) {
|
|
82
|
+
const {
|
|
83
|
+
minionsHome,
|
|
84
|
+
dashboardUrl = 'http://127.0.0.1:7331/api/health',
|
|
85
|
+
readControl = readEngineControl,
|
|
86
|
+
isProcessAlive: isAlive = isProcessAlive,
|
|
87
|
+
httpGetJson: getJson = httpGetJson,
|
|
88
|
+
} = options;
|
|
89
|
+
|
|
90
|
+
const control = readControl(minionsHome);
|
|
91
|
+
const pid = normalizePid(control && control.pid);
|
|
92
|
+
const engineAlive = pid ? isAlive(pid) : false;
|
|
93
|
+
const engineOk = control && control.state === 'running' && engineAlive;
|
|
94
|
+
|
|
95
|
+
const dashboard = await getJson(dashboardUrl, 1000);
|
|
96
|
+
const dashboardStatus = dashboard && dashboard.json && dashboard.json.status;
|
|
97
|
+
const dashboardOk = !!(dashboard && dashboard.ok && dashboardStatus === 'healthy');
|
|
98
|
+
|
|
99
|
+
const errors = [];
|
|
100
|
+
if (!engineOk) {
|
|
101
|
+
const state = control && control.state ? control.state : 'unknown';
|
|
102
|
+
const pidLabel = pid || 'none';
|
|
103
|
+
errors.push(`Engine is not healthy (state=${state}, pid=${pidLabel}, alive=${engineAlive ? 'yes' : 'no'})`);
|
|
104
|
+
}
|
|
105
|
+
if (!dashboardOk) {
|
|
106
|
+
const detail = dashboard && dashboard.error
|
|
107
|
+
? dashboard.error.message
|
|
108
|
+
: dashboard && dashboard.statusCode
|
|
109
|
+
? `HTTP ${dashboard.statusCode}${dashboardStatus ? `, status=${dashboardStatus}` : ''}`
|
|
110
|
+
: 'no response';
|
|
111
|
+
errors.push(`Dashboard failed health check at ${dashboardUrl} (${detail})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: engineOk && dashboardOk,
|
|
116
|
+
engine: { state: control && control.state, pid, alive: engineAlive },
|
|
117
|
+
dashboard: { url: dashboardUrl, ok: !!(dashboard && dashboard.ok), statusCode: dashboard && dashboard.statusCode, status: dashboardStatus },
|
|
118
|
+
errors,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function waitForRestartHealth(options = {}) {
|
|
123
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_RESTART_HEALTH_TIMEOUT_MS;
|
|
124
|
+
const intervalMs = options.intervalMs ?? DEFAULT_RESTART_HEALTH_INTERVAL_MS;
|
|
125
|
+
const maxAttempts = normalizePid(options.maxAttempts);
|
|
126
|
+
const started = Date.now();
|
|
127
|
+
let attempts = 0;
|
|
128
|
+
let last = null;
|
|
129
|
+
|
|
130
|
+
while (true) {
|
|
131
|
+
attempts++;
|
|
132
|
+
last = await checkRestartHealth(options);
|
|
133
|
+
last.attempts = attempts;
|
|
134
|
+
last.elapsedMs = Date.now() - started;
|
|
135
|
+
if (last.ok) return last;
|
|
136
|
+
if (maxAttempts && attempts >= maxAttempts) break;
|
|
137
|
+
const remainingMs = timeoutMs - (Date.now() - started);
|
|
138
|
+
if (!maxAttempts && remainingMs <= 0) break;
|
|
139
|
+
await sleep(maxAttempts ? intervalMs : Math.min(intervalMs, remainingMs));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return last || {
|
|
143
|
+
ok: false,
|
|
144
|
+
attempts,
|
|
145
|
+
elapsedMs: Date.now() - started,
|
|
146
|
+
errors: ['Restart health check did not run'],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatRestartHealthError(result) {
|
|
151
|
+
const elapsed = typeof result.elapsedMs === 'number' ? `${result.elapsedMs}ms` : 'unknown time';
|
|
152
|
+
const attempts = result.attempts || 0;
|
|
153
|
+
const details = (result.errors || ['Unknown restart verification failure']).map(err => ` - ${err}`).join('\n');
|
|
154
|
+
return `\n ERROR: Restart verification failed after ${elapsed} (${attempts} attempt${attempts === 1 ? '' : 's'}).\n${details}\n`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
DEFAULT_RESTART_HEALTH_TIMEOUT_MS,
|
|
159
|
+
DEFAULT_RESTART_HEALTH_INTERVAL_MS,
|
|
160
|
+
checkRestartHealth,
|
|
161
|
+
waitForRestartHealth,
|
|
162
|
+
formatRestartHealthError,
|
|
163
|
+
_private: {
|
|
164
|
+
httpGetJson,
|
|
165
|
+
isProcessAlive,
|
|
166
|
+
readEngineControl,
|
|
167
|
+
normalizePid,
|
|
168
|
+
},
|
|
169
|
+
};
|
package/engine/shared.js
CHANGED
|
@@ -2209,7 +2209,8 @@ function sanitizePath(file, baseDir) {
|
|
|
2209
2209
|
if (path.isAbsolute(file) || /^[a-zA-Z]:/.test(file)) throw new Error('invalid file path: absolute path not allowed');
|
|
2210
2210
|
const resolved = path.resolve(baseDir, file);
|
|
2211
2211
|
const normalizedBase = path.resolve(baseDir);
|
|
2212
|
-
|
|
2212
|
+
const rel = path.relative(normalizedBase, resolved);
|
|
2213
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
2213
2214
|
throw new Error('invalid file path: outside allowed directory');
|
|
2214
2215
|
}
|
|
2215
2216
|
return resolved;
|
package/engine.js
CHANGED
|
@@ -2015,8 +2015,11 @@ function safePrdProjectSlug(projectName) {
|
|
|
2015
2015
|
|
|
2016
2016
|
function safePrdFilenameForProject(projectName, suffix) {
|
|
2017
2017
|
const fileName = `${safePrdProjectSlug(projectName)}-${suffix}.json`;
|
|
2018
|
-
shared.sanitizePath(fileName, PRD_DIR);
|
|
2019
|
-
|
|
2018
|
+
const resolved = shared.sanitizePath(fileName, PRD_DIR);
|
|
2019
|
+
if (path.dirname(resolved) !== path.resolve(PRD_DIR)) {
|
|
2020
|
+
throw new Error('invalid PRD filename: nested paths are not allowed');
|
|
2021
|
+
}
|
|
2022
|
+
return path.basename(resolved);
|
|
2020
2023
|
}
|
|
2021
2024
|
|
|
2022
2025
|
function materializePlansAsWorkItems(config) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1815",
|
|
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"
|