@yemi33/minions 0.1.1813 → 0.1.1814
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/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/shared.js +2 -1
- package/engine.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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
|
|
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.1814",
|
|
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"
|