@yemi33/minions 0.1.601 → 0.1.602
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 +4 -1
- package/dashboard/js/render-pipelines.js +23 -6
- package/dashboard.js +4 -2
- package/engine/pipeline.js +137 -4
- package/engine/shared.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -191,13 +191,13 @@ function renderPipelines(pipelines) {
|
|
|
191
191
|
el.innerHTML = pipelines.map(function(p) {
|
|
192
192
|
const activeRun = (p.runs || []).find(function(r) { return r.status === 'running'; });
|
|
193
193
|
const lastRun = (p.runs || []).slice(-1)[0];
|
|
194
|
-
const statusColor = activeRun ? 'var(--blue)' : lastRun?.status === 'completed' ? 'var(--green)' : lastRun?.status === 'failed' ? 'var(--red)' : 'var(--muted)';
|
|
195
|
-
const statusLabel = activeRun ? 'Running' : lastRun ? (lastRun.status === 'completed' ? 'Completed' : lastRun.status === 'failed' ? 'Failed' : lastRun.status) : 'Never run';
|
|
194
|
+
const statusColor = activeRun ? 'var(--blue)' : lastRun?.status === 'completed' ? 'var(--green)' : lastRun?.status === 'failed' ? 'var(--red)' : lastRun?.status === 'stopped' ? 'var(--yellow)' : 'var(--muted)';
|
|
195
|
+
const statusLabel = activeRun ? 'Running' : lastRun ? (lastRun.status === 'completed' ? 'Completed' : lastRun.status === 'failed' ? 'Failed' : lastRun.status === 'stopped' ? 'Stopped' : lastRun.status) : 'Never run';
|
|
196
196
|
const trigger = p.trigger?.cron ? _cronToHuman(p.trigger.cron) : 'Manual';
|
|
197
197
|
|
|
198
198
|
// Stage flow visualization
|
|
199
199
|
var stageFlow = (p.stages || []).map(function(s) {
|
|
200
|
-
var icon = { task: '\u2699', meeting: '\uD83D\uDCAC', plan: '\uD83D\uDCCB', 'merge-prs': '\uD83D\uDD00', api: '\uD83C\uDF10', wait: '\u23F8', parallel: '\u2693', schedule: '\u23F0' }[s.type] || '\u2022';
|
|
200
|
+
var icon = { task: '\u2699', meeting: '\uD83D\uDCAC', plan: '\uD83D\uDCCB', 'merge-prs': '\uD83D\uDD00', api: '\uD83C\uDF10', wait: '\u23F8', parallel: '\u2693', schedule: '\u23F0', condition: '\u2753' }[s.type] || '\u2022';
|
|
201
201
|
var stageStatus = activeRun?.stages?.[s.id]?.status || 'pending';
|
|
202
202
|
var color = stageStatus === 'completed' ? 'var(--green)' : stageStatus === 'running' ? 'var(--blue)' : stageStatus === 'failed' ? 'var(--red)' : stageStatus === 'waiting-human' ? 'var(--yellow)' : 'var(--muted)';
|
|
203
203
|
return '<span style="color:' + color + ';font-size:11px" title="' + escHtml(s.id) + ': ' + escHtml(s.title || s.type) + ' (' + stageStatus + ')">' + icon + ' ' + escHtml(s.id) + '</span>';
|
|
@@ -220,7 +220,8 @@ function renderPipelines(pipelines) {
|
|
|
220
220
|
'<div style="display:flex;align-items:center;gap:8px">' +
|
|
221
221
|
'<span style="color:' + statusColor + ';font-size:11px;font-weight:600">' + statusLabel + '</span>' +
|
|
222
222
|
'<span style="font-size:10px;color:var(--muted)">' + escHtml(trigger) + '</span>' +
|
|
223
|
-
(p.
|
|
223
|
+
(p.stopWhen ? '<span style="font-size:9px;color:var(--yellow)" title="Auto-stops when condition met: ' + escHtml(typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || 'condition')) + '">STOP-WHEN</span>' : '') +
|
|
224
|
+
(p.enabled === false ? '<span style="font-size:9px;color:var(--red)"' + (p._stopReason ? ' title="' + escHtml(p._stopReason) + '"' : '') + '>' + (p._stoppedBy ? 'AUTO-STOPPED' : 'DISABLED') + '</span>' : '') +
|
|
224
225
|
'</div>' +
|
|
225
226
|
'</div>' +
|
|
226
227
|
'<div style="margin-top:6px;display:flex;gap:4px;align-items:center;flex-wrap:wrap">' + stageFlow + '</div>' +
|
|
@@ -266,6 +267,20 @@ function openPipelineDetail(id) {
|
|
|
266
267
|
'</div>';
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// stopWhen info
|
|
271
|
+
if (p.stopWhen) {
|
|
272
|
+
var swLabel = typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || JSON.stringify(p.stopWhen));
|
|
273
|
+
html += '<div style="border:1px solid color-mix(in srgb, var(--yellow) 30%, transparent);border-radius:6px;padding:4px 10px;background:color-mix(in srgb, var(--yellow) 6%, transparent);font-size:11px">' +
|
|
274
|
+
'<span style="color:var(--yellow);font-weight:600">Stop When:</span> <span style="color:var(--text)">' + escHtml(swLabel) + '</span>' +
|
|
275
|
+
(p._stoppedBy ? ' <span style="color:var(--green);font-size:10px">\u2714 triggered' + (p._stoppedAt ? ' at ' + escHtml(p._stoppedAt.slice(0, 16).replace('T', ' ')) : '') + '</span>' : '') +
|
|
276
|
+
'</div>';
|
|
277
|
+
}
|
|
278
|
+
if (p._stopReason && p.enabled === false) {
|
|
279
|
+
html += '<div style="border:1px solid color-mix(in srgb, var(--red) 30%, transparent);border-radius:6px;padding:4px 10px;background:color-mix(in srgb, var(--red) 6%, transparent);font-size:11px">' +
|
|
280
|
+
'<span style="color:var(--red);font-weight:600">Auto-stopped:</span> <span style="color:var(--text)">' + escHtml(p._stopReason) + '</span>' +
|
|
281
|
+
'</div>';
|
|
282
|
+
}
|
|
283
|
+
|
|
269
284
|
html += '<h4 style="font-size:12px;color:var(--blue);margin:0">Stages</h4>';
|
|
270
285
|
(p.stages || []).forEach(function(s, i) {
|
|
271
286
|
var stageRun = activeRun?.stages?.[s.id] || {};
|
|
@@ -278,7 +293,9 @@ function openPipelineDetail(id) {
|
|
|
278
293
|
'<span style="font-weight:600;font-size:12px">' + (i + 1) + '. ' + escHtml(s.title || s.id) + '</span>' +
|
|
279
294
|
'<span style="color:' + statusColor + ';font-size:10px;font-weight:600">' + stageStatus.toUpperCase() + '</span>' +
|
|
280
295
|
'</div>' +
|
|
281
|
-
'<div style="font-size:10px;color:var(--muted);margin-top:4px">Type: ' + escHtml(s.type) + ' | Depends on: ' + escHtml(deps) + (s.agent ? ' | Agent: ' + escHtml(s.agent) : '') +
|
|
296
|
+
'<div style="font-size:10px;color:var(--muted);margin-top:4px">Type: ' + escHtml(s.type) + ' | Depends on: ' + escHtml(deps) + (s.agent ? ' | Agent: ' + escHtml(s.agent) : '') +
|
|
297
|
+
(s.type === 'condition' ? ' | Check: ' + escHtml(typeof (s.check || s.condition) === 'string' ? (s.check || s.condition) : ((s.check || s.condition || {}).check || '?')) + (s.onMet ? ' | onMet: ' + escHtml(s.onMet) : '') + (s.onUnmet ? ' | onUnmet: ' + escHtml(s.onUnmet) : '') : '') +
|
|
298
|
+
'</div>' +
|
|
282
299
|
_renderMonitoredResources(s.monitoredResources || []) +
|
|
283
300
|
_renderArtifactLinks(stageRun.artifacts) +
|
|
284
301
|
(stageRun.output ? '<div style="margin-top:6px;font-size:11px;max-height:150px;overflow-y:auto">' + renderMd(stageRun.output.slice(0, 500)) + '</div>' : '') +
|
|
@@ -291,7 +308,7 @@ function openPipelineDetail(id) {
|
|
|
291
308
|
if (runs.length > 0) {
|
|
292
309
|
html += '<h4 style="font-size:12px;color:var(--blue);margin:0">Recent Runs</h4>';
|
|
293
310
|
runs.forEach(function(r, ri) {
|
|
294
|
-
var color = r.status === 'completed' ? 'var(--green)' : r.status === 'failed' ? 'var(--red)' : r.status === 'running' ? 'var(--blue)' : 'var(--muted)';
|
|
311
|
+
var color = r.status === 'completed' ? 'var(--green)' : r.status === 'failed' ? 'var(--red)' : r.status === 'running' ? 'var(--blue)' : r.status === 'stopped' ? 'var(--yellow)' : 'var(--muted)';
|
|
295
312
|
// Collect all artifacts across stages for this run
|
|
296
313
|
var runArtifacts = _collectRunArtifacts(r);
|
|
297
314
|
var artifactCount = runArtifacts.total;
|
package/dashboard.js
CHANGED
|
@@ -4020,18 +4020,19 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4020
4020
|
const result = pipelines.map(p => ({ ...p, runs: (runs[p.id] || []).slice(-5) }));
|
|
4021
4021
|
return jsonReply(res, 200, result);
|
|
4022
4022
|
}},
|
|
4023
|
-
{ method: 'POST', path: '/api/pipelines', desc: 'Create a pipeline', params: 'id, title, stages[], trigger?, monitoredResources?', handler: async (req, res) => {
|
|
4023
|
+
{ method: 'POST', path: '/api/pipelines', desc: 'Create a pipeline', params: 'id, title, stages[], trigger?, stopWhen?, monitoredResources?', handler: async (req, res) => {
|
|
4024
4024
|
const body = await readBody(req);
|
|
4025
4025
|
if (!body.id || !body.title || !body.stages) return jsonReply(res, 400, { error: 'id, title, and stages required' });
|
|
4026
4026
|
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
4027
4027
|
if (getPipeline(body.id)) return jsonReply(res, 409, { error: 'Pipeline already exists' });
|
|
4028
4028
|
const pipeline = { id: body.id, title: body.title, stages: body.stages, trigger: body.trigger || {}, enabled: body.enabled !== false };
|
|
4029
|
+
if (body.stopWhen) pipeline.stopWhen = body.stopWhen;
|
|
4029
4030
|
if (Array.isArray(body.monitoredResources) && body.monitoredResources.length > 0) pipeline.monitoredResources = body.monitoredResources;
|
|
4030
4031
|
savePipeline(pipeline);
|
|
4031
4032
|
invalidateStatusCache();
|
|
4032
4033
|
return jsonReply(res, 200, { ok: true, id: pipeline.id });
|
|
4033
4034
|
}},
|
|
4034
|
-
{ method: 'POST', path: '/api/pipelines/update', desc: 'Update a pipeline', params: 'id, title?, stages?, trigger?, enabled?, monitoredResources?', handler: async (req, res) => {
|
|
4035
|
+
{ method: 'POST', path: '/api/pipelines/update', desc: 'Update a pipeline', params: 'id, title?, stages?, trigger?, enabled?, stopWhen?, monitoredResources?', handler: async (req, res) => {
|
|
4035
4036
|
const body = await readBody(req);
|
|
4036
4037
|
if (!body.id) return jsonReply(res, 400, { error: 'id required' });
|
|
4037
4038
|
const { getPipeline, savePipeline } = require('./engine/pipeline');
|
|
@@ -4042,6 +4043,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4042
4043
|
if (body.trigger !== undefined) pipeline.trigger = body.trigger;
|
|
4043
4044
|
if (body.enabled !== undefined) pipeline.enabled = body.enabled;
|
|
4044
4045
|
if (body.monitoredResources !== undefined) pipeline.monitoredResources = body.monitoredResources;
|
|
4046
|
+
if (body.stopWhen !== undefined) pipeline.stopWhen = body.stopWhen;
|
|
4045
4047
|
savePipeline(pipeline);
|
|
4046
4048
|
invalidateStatusCache();
|
|
4047
4049
|
return jsonReply(res, 200, { ok: true });
|
package/engine/pipeline.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* engine/pipeline.js — Multi-stage pipeline orchestration.
|
|
3
|
-
* Pipelines chain stages (task, meeting, plan, merge-prs, api, wait, parallel)
|
|
4
|
-
* with dependency tracking and
|
|
3
|
+
* Pipelines chain stages (task, meeting, plan, merge-prs, api, wait, parallel, condition)
|
|
4
|
+
* with dependency tracking, artifact discovery, and conditional stop/termination.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
@@ -124,6 +124,79 @@ function resolveStageConfig(stage, run) {
|
|
|
124
124
|
return resolved;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// ── Condition Evaluation ─────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Evaluate a pipeline condition. Used by both `stopWhen` and `condition` stages.
|
|
131
|
+
* @param {string|object} condition — condition name (string shorthand) or { check, ... } object
|
|
132
|
+
* @param {{ run, pipeline, config }} ctx — evaluation context
|
|
133
|
+
* @returns {boolean} whether the condition is met
|
|
134
|
+
*/
|
|
135
|
+
function evaluateCondition(condition, ctx) {
|
|
136
|
+
const { run, pipeline, config } = ctx;
|
|
137
|
+
// Normalize: string shorthand → { check: string }
|
|
138
|
+
const cond = typeof condition === 'string' ? { check: condition } : (condition || {});
|
|
139
|
+
|
|
140
|
+
switch (cond.check) {
|
|
141
|
+
case 'runSucceeded': {
|
|
142
|
+
// True when the current/last run completed with all stages succeeded (none failed)
|
|
143
|
+
if (!run) return false;
|
|
144
|
+
return Object.values(run.stages || {}).every(
|
|
145
|
+
s => s.status === PIPELINE_STATUS.COMPLETED || s.status === PIPELINE_STATUS.PENDING
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
case 'noFailedItems': {
|
|
149
|
+
// True when all work items created by the pipeline are done (not failed)
|
|
150
|
+
if (!run) return false;
|
|
151
|
+
const wiPath = path.join(__dirname, '..', 'work-items.json');
|
|
152
|
+
const workItems = safeJson(wiPath) || [];
|
|
153
|
+
const allProjectWi = shared.getProjects(config).reduce((acc, p) => {
|
|
154
|
+
return acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []);
|
|
155
|
+
}, []);
|
|
156
|
+
const all = [...workItems, ...allProjectWi];
|
|
157
|
+
const pipelineWis = all.filter(w => w._pipelineRun === run.runId);
|
|
158
|
+
if (pipelineWis.length === 0) return true; // no items = nothing failed
|
|
159
|
+
return pipelineWis.every(w => w.status !== WI_STATUS.FAILED);
|
|
160
|
+
}
|
|
161
|
+
case 'maxRuns': {
|
|
162
|
+
// True when total run count for this pipeline >= threshold
|
|
163
|
+
const threshold = cond.value || 1;
|
|
164
|
+
const runs = getPipelineRuns();
|
|
165
|
+
const pipelineRuns = runs[pipeline?.id] || [];
|
|
166
|
+
return pipelineRuns.length >= threshold;
|
|
167
|
+
}
|
|
168
|
+
case 'allBuildsGreen': {
|
|
169
|
+
// True when all PRs in monitoredResources (or run artifacts) have buildStatus 'passing'
|
|
170
|
+
const projects = shared.getProjects(config);
|
|
171
|
+
const allPrs = projects.reduce((acc, p) => {
|
|
172
|
+
return acc.concat(safeJson(shared.projectPrPath(p)) || []);
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
// Collect PR IDs to check: from monitoredResources (type: 'pr') or run artifact PRs
|
|
176
|
+
const prIds = new Set();
|
|
177
|
+
for (const res of (pipeline?.monitoredResources || [])) {
|
|
178
|
+
const r = typeof res === 'string' ? { label: res } : res;
|
|
179
|
+
if (r.type === 'pr' && r.label) prIds.add(r.label);
|
|
180
|
+
}
|
|
181
|
+
// Also check PRs from run artifacts
|
|
182
|
+
if (run) {
|
|
183
|
+
for (const [, s] of Object.entries(run.stages || {})) {
|
|
184
|
+
for (const prId of (s.artifacts?.prs || [])) prIds.add(prId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (prIds.size === 0) return false; // no PRs to check = can't confirm green
|
|
188
|
+
for (const prId of prIds) {
|
|
189
|
+
const pr = allPrs.find(p => p.id === prId);
|
|
190
|
+
if (!pr || pr.buildStatus !== 'passing') return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
log('warn', `Pipeline condition: unknown check '${cond.check}'`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
127
200
|
// ── Stage Execution ──────────────────────────────────────────────────────────
|
|
128
201
|
|
|
129
202
|
async function executeStage(stage, run, pipeline, config) {
|
|
@@ -143,6 +216,8 @@ async function executeStage(stage, run, pipeline, config) {
|
|
|
143
216
|
return executeMergePrsStage(resolved, stageState, run, config);
|
|
144
217
|
case STAGE_TYPE.SCHEDULE:
|
|
145
218
|
return executeScheduleStage(resolved, stageState, config);
|
|
219
|
+
case STAGE_TYPE.CONDITION:
|
|
220
|
+
return executeConditionStage(resolved, stageState, run, pipeline, config);
|
|
146
221
|
case STAGE_TYPE.WAIT:
|
|
147
222
|
// wait stages just sit in waiting-human status until continued via API
|
|
148
223
|
return { status: PIPELINE_STATUS.WAITING_HUMAN };
|
|
@@ -375,6 +450,38 @@ function executeScheduleStage(stage, stageState, config) {
|
|
|
375
450
|
return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts() };
|
|
376
451
|
}
|
|
377
452
|
|
|
453
|
+
function executeConditionStage(stage, stageState, run, pipeline, config) {
|
|
454
|
+
const check = stage.check || stage.condition;
|
|
455
|
+
if (!check) {
|
|
456
|
+
log('warn', `Pipeline ${pipeline.id}: condition stage ${stage.id} has no check defined`);
|
|
457
|
+
return { status: PIPELINE_STATUS.FAILED, error: 'no check defined' };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const met = evaluateCondition(check, { run, pipeline, config });
|
|
461
|
+
const action = met ? (stage.onMet || 'complete') : (stage.onUnmet || 'fail');
|
|
462
|
+
log('info', `Pipeline ${pipeline.id}: condition '${typeof check === 'string' ? check : check.check}' → ${met ? 'met' : 'unmet'}, action: ${action}`);
|
|
463
|
+
|
|
464
|
+
switch (action) {
|
|
465
|
+
case 'complete':
|
|
466
|
+
case 'continue':
|
|
467
|
+
return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts(), output: `Condition ${met ? 'met' : 'unmet'}` };
|
|
468
|
+
case 'fail':
|
|
469
|
+
return { status: PIPELINE_STATUS.FAILED, error: `Condition ${met ? 'met' : 'unmet'}` };
|
|
470
|
+
case 'stop-pipeline':
|
|
471
|
+
// Disable the pipeline and complete the run
|
|
472
|
+
pipeline.enabled = false;
|
|
473
|
+
pipeline._stoppedBy = stage.id;
|
|
474
|
+
pipeline._stoppedAt = ts();
|
|
475
|
+
pipeline._stopReason = `Condition '${typeof check === 'string' ? check : check.check}' ${met ? 'met' : 'unmet'}`;
|
|
476
|
+
savePipeline(pipeline);
|
|
477
|
+
log('info', `Pipeline ${pipeline.id}: auto-disabled by condition stage ${stage.id}`);
|
|
478
|
+
return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts(), output: `Pipeline stopped: condition ${met ? 'met' : 'unmet'}`, _stopPipeline: true };
|
|
479
|
+
default:
|
|
480
|
+
log('warn', `Pipeline ${pipeline.id}: unknown condition action '${action}'`);
|
|
481
|
+
return { status: PIPELINE_STATUS.FAILED, error: `unknown action '${action}'` };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
378
485
|
async function executeParallelStage(stage, stageState, run, pipeline, config) {
|
|
379
486
|
const subStages = stage.stages || [];
|
|
380
487
|
const subResults = {};
|
|
@@ -496,7 +603,8 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
496
603
|
}
|
|
497
604
|
case STAGE_TYPE.API:
|
|
498
605
|
case STAGE_TYPE.SCHEDULE:
|
|
499
|
-
|
|
606
|
+
case STAGE_TYPE.CONDITION:
|
|
607
|
+
return true; // immediate — resolved synchronously on execute
|
|
500
608
|
case STAGE_TYPE.WAIT:
|
|
501
609
|
return stageState.status === PIPELINE_STATUS.COMPLETED;
|
|
502
610
|
case STAGE_TYPE.PARALLEL: {
|
|
@@ -626,6 +734,12 @@ async function discoverPipelineWork(config) {
|
|
|
626
734
|
updateRunStage(pipeline.id, activeRun.runId, stage.id, { ...result, startedAt: ts() });
|
|
627
735
|
Object.assign(stageState, result, { startedAt: ts() });
|
|
628
736
|
if (result.status === PIPELINE_STATUS.RUNNING) anyRunning = true;
|
|
737
|
+
// Condition stage signaled pipeline stop — complete the run immediately
|
|
738
|
+
if (result._stopPipeline) {
|
|
739
|
+
completeRun(pipeline.id, activeRun.runId, PIPELINE_STATUS.STOPPED);
|
|
740
|
+
allComplete = true;
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
629
743
|
log('info', `Pipeline ${pipeline.id}: started stage ${stage.id} (${stage.type})`);
|
|
630
744
|
}
|
|
631
745
|
}
|
|
@@ -636,7 +750,25 @@ async function discoverPipelineWork(config) {
|
|
|
636
750
|
|
|
637
751
|
// Check if run is done
|
|
638
752
|
if (allComplete) {
|
|
639
|
-
|
|
753
|
+
// Only complete if not already stopped by a condition stage
|
|
754
|
+
if (activeRun.status !== PIPELINE_STATUS.STOPPED) {
|
|
755
|
+
completeRun(pipeline.id, activeRun.runId, PIPELINE_STATUS.COMPLETED);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Evaluate top-level stopWhen after successful run completion
|
|
759
|
+
if (pipeline.stopWhen && pipeline.enabled !== false) {
|
|
760
|
+
try {
|
|
761
|
+
const stopMet = evaluateCondition(pipeline.stopWhen, { run: activeRun, pipeline, config });
|
|
762
|
+
if (stopMet) {
|
|
763
|
+
pipeline.enabled = false;
|
|
764
|
+
pipeline._stoppedBy = 'stopWhen';
|
|
765
|
+
pipeline._stoppedAt = ts();
|
|
766
|
+
pipeline._stopReason = `stopWhen condition '${typeof pipeline.stopWhen === 'string' ? pipeline.stopWhen : (pipeline.stopWhen.check || 'unknown')}' met`;
|
|
767
|
+
savePipeline(pipeline);
|
|
768
|
+
log('info', `Pipeline ${pipeline.id}: auto-disabled by stopWhen condition`);
|
|
769
|
+
}
|
|
770
|
+
} catch (e) { log('warn', `Pipeline ${pipeline.id}: stopWhen evaluation failed: ${e.message}`); }
|
|
771
|
+
}
|
|
640
772
|
} else if (anyFailed && !anyRunning) {
|
|
641
773
|
completeRun(pipeline.id, activeRun.runId, PIPELINE_STATUS.FAILED);
|
|
642
774
|
}
|
|
@@ -648,4 +780,5 @@ module.exports = {
|
|
|
648
780
|
getPipelines, getPipeline, savePipeline, deletePipeline,
|
|
649
781
|
getPipelineRuns, getActiveRun, startRun, updateRunStage, completeRun,
|
|
650
782
|
discoverPipelineWork,
|
|
783
|
+
evaluateCondition, // exported for testing
|
|
651
784
|
};
|
package/engine/shared.js
CHANGED
|
@@ -558,10 +558,12 @@ const DISPATCH_RESULT = { SUCCESS: 'success', ERROR: 'error', TIMEOUT: 'timeout'
|
|
|
558
558
|
const PIPELINE_STATUS = {
|
|
559
559
|
PENDING: 'pending', RUNNING: 'running', COMPLETED: 'completed',
|
|
560
560
|
FAILED: 'failed', PAUSED: 'paused', WAITING_HUMAN: 'waiting-human',
|
|
561
|
+
STOPPED: 'stopped', // pipeline auto-disabled by stopWhen or condition stage
|
|
561
562
|
};
|
|
562
563
|
const STAGE_TYPE = {
|
|
563
564
|
TASK: 'task', MEETING: 'meeting', PLAN: 'plan', API: 'api',
|
|
564
565
|
MERGE_PRS: 'merge-prs', SCHEDULE: 'schedule', WAIT: 'wait', PARALLEL: 'parallel',
|
|
566
|
+
CONDITION: 'condition',
|
|
565
567
|
};
|
|
566
568
|
const MEETING_STATUS = {
|
|
567
569
|
INVESTIGATING: 'investigating', DEBATING: 'debating', CONCLUDING: 'concluding',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.602",
|
|
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"
|