@yemi33/minions 0.1.1670 → 0.1.1671
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 +14 -2
- package/dashboard/js/render-pipelines.js +1 -0
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +1 -1
- package/engine/meeting.js +83 -5
- package/engine/routing.js +2 -1
- package/engine/shared.js +5 -2
- package/engine/spawn-agent.js +12 -5
- package/engine.js +11 -4
- package/package.json +1 -1
- package/playbooks/shared-rules.md +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1671 (2026-05-02)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- durable meeting artifact ingestion (#1972)
|
|
7
|
+
|
|
8
|
+
### Fixes
|
|
9
|
+
- shared-rules.md — declare done after push, do not wait for remote pipelines (#1970)
|
|
10
|
+
- sync pipeline card with modal after retrigger/abort/continue
|
|
11
|
+
- harden maxConcurrent parsing, routing case, and runtime exit codes
|
|
12
|
+
|
|
13
|
+
### Other
|
|
14
|
+
- docs: condense CLAUDE.md
|
|
15
|
+
|
|
16
|
+
## 0.1.1669 (2026-05-02)
|
|
4
17
|
|
|
5
18
|
### Features
|
|
6
|
-
- make ADO PR titles authoritative (#1965)
|
|
7
19
|
- gate pendingFix clear (#1964)
|
|
8
20
|
|
|
9
21
|
## 0.1.1668 (2026-05-01)
|
|
@@ -434,6 +434,7 @@ async function _refreshPipelineDetail(id) {
|
|
|
434
434
|
if (fresh) {
|
|
435
435
|
_pipelinesData = _pipelinesData.map(function(x) { return x.id === id ? fresh : x; });
|
|
436
436
|
_pipelinePollHash = JSON.stringify({ runs: fresh.runs || [], enabled: fresh.enabled, _stoppedBy: fresh._stoppedBy, _stopReason: fresh._stopReason });
|
|
437
|
+
renderPipelines(_pipelinesData);
|
|
437
438
|
openPipelineDetail(id);
|
|
438
439
|
}
|
|
439
440
|
} catch (e) { /* silent — next poll will catch up */ }
|
package/engine/lifecycle.js
CHANGED
|
@@ -2529,7 +2529,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2529
2529
|
if (type === WORK_TYPE.MEETING && meta?.meetingId) {
|
|
2530
2530
|
try {
|
|
2531
2531
|
const { collectMeetingFindings } = require('./meeting');
|
|
2532
|
-
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
|
|
2532
|
+
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion);
|
|
2533
2533
|
} catch (err) { log('warn', `Meeting collect: ${err.message}`); }
|
|
2534
2534
|
}
|
|
2535
2535
|
|
package/engine/meeting.js
CHANGED
|
@@ -19,6 +19,86 @@ const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
|
|
|
19
19
|
// Derive from shared.MINIONS_DIR so createTestMinionsDir()/MINIONS_TEST_DIR
|
|
20
20
|
// tests can redirect the meetings directory without patching module internals.
|
|
21
21
|
const MEETINGS_DIR = path.join(shared.MINIONS_DIR, 'meetings');
|
|
22
|
+
const MEETING_NOTE_ARTIFACT_ROOT = path.join(shared.MINIONS_DIR, 'notes', 'inbox');
|
|
23
|
+
|
|
24
|
+
function isEmptyMeetingContent(text) {
|
|
25
|
+
const value = String(text || '').trim();
|
|
26
|
+
return !value || EMPTY_OUTPUT_PATTERNS.includes(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isSuccessfulStructuredCompletion(completion) {
|
|
30
|
+
const status = String(completion?.status || completion?.outcome || '').trim().toLowerCase();
|
|
31
|
+
return ['success', 'succeeded', 'complete', 'completed', 'done', 'ok', 'passed'].includes(status);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getStructuredNoteArtifacts(structuredCompletion) {
|
|
35
|
+
const artifacts = structuredCompletion?.artifacts;
|
|
36
|
+
if (!Array.isArray(artifacts)) return [];
|
|
37
|
+
return artifacts.filter(artifact =>
|
|
38
|
+
artifact &&
|
|
39
|
+
typeof artifact === 'object' &&
|
|
40
|
+
String(artifact.type || '').toLowerCase() === 'note' &&
|
|
41
|
+
typeof artifact.path === 'string' &&
|
|
42
|
+
artifact.path.trim()
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isPathInside(parent, child) {
|
|
47
|
+
const rel = path.relative(parent, child);
|
|
48
|
+
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveMeetingNoteArtifactPath(artifactPath) {
|
|
52
|
+
const raw = String(artifactPath || '').trim();
|
|
53
|
+
if (!raw || raw.includes('\0')) return null;
|
|
54
|
+
const resolved = path.resolve(path.isAbsolute(raw) ? raw : path.join(shared.MINIONS_DIR, raw));
|
|
55
|
+
const root = path.resolve(MEETING_NOTE_ARTIFACT_ROOT);
|
|
56
|
+
if (!isPathInside(root, resolved)) return null;
|
|
57
|
+
if (path.extname(resolved).toLowerCase() !== '.md') return null;
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readMeetingNoteArtifact(artifactPath) {
|
|
62
|
+
const resolved = resolveMeetingNoteArtifactPath(artifactPath);
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
log('warn', `Ignoring unsafe meeting note artifact path: ${artifactPath || '(empty)'}`);
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const realRoot = fs.realpathSync(MEETING_NOTE_ARTIFACT_ROOT);
|
|
69
|
+
const realPath = fs.realpathSync(resolved);
|
|
70
|
+
if (!isPathInside(realRoot, realPath)) {
|
|
71
|
+
log('warn', `Ignoring meeting note artifact outside notes/inbox: ${artifactPath}`);
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
const content = fs.readFileSync(realPath, 'utf8');
|
|
75
|
+
return isEmptyMeetingContent(content) ? '' : content;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
log('warn', `Meeting note artifact unreadable (${artifactPath}): ${err.message}`);
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveStructuredMeetingContent(structuredCompletion) {
|
|
83
|
+
if (!isSuccessfulStructuredCompletion(structuredCompletion)) return '';
|
|
84
|
+
const noteArtifacts = getStructuredNoteArtifacts(structuredCompletion);
|
|
85
|
+
if (noteArtifacts.length === 0) return '';
|
|
86
|
+
|
|
87
|
+
for (const artifact of noteArtifacts) {
|
|
88
|
+
const content = readMeetingNoteArtifact(artifact.path);
|
|
89
|
+
if (content) return content;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const summary = String(structuredCompletion.summary || '').trim();
|
|
93
|
+
return isEmptyMeetingContent(summary) ? '' : summary;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveMeetingContributionContent(output, structuredCompletion) {
|
|
97
|
+
const { text } = shared.parseStreamJsonOutput(output, { maxTextLength: 50000 });
|
|
98
|
+
const rawContent = (text || '').trim();
|
|
99
|
+
if (!isEmptyMeetingContent(rawContent)) return rawContent;
|
|
100
|
+
return resolveStructuredMeetingContent(structuredCompletion);
|
|
101
|
+
}
|
|
22
102
|
|
|
23
103
|
function truncateMeetingContext(text, maxBytes, label) {
|
|
24
104
|
return shared.truncateTextBytes(text, maxBytes, `\n\n_...${label} truncated — review the meeting transcript if needed._`);
|
|
@@ -323,7 +403,7 @@ function discoverMeetingWork(config) {
|
|
|
323
403
|
* Collect findings from a completed meeting agent.
|
|
324
404
|
* Called from runPostCompletionHooks when type === 'meeting'.
|
|
325
405
|
*/
|
|
326
|
-
function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
406
|
+
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null) {
|
|
327
407
|
const meeting = getMeeting(meetingId);
|
|
328
408
|
if (!meeting) return;
|
|
329
409
|
if (meeting.status === 'completed' || meeting.status === 'archived') {
|
|
@@ -331,17 +411,15 @@ function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
|
331
411
|
return;
|
|
332
412
|
}
|
|
333
413
|
|
|
334
|
-
const
|
|
335
|
-
const rawContent = (text || '').trim();
|
|
414
|
+
const content = resolveMeetingContributionContent(output, structuredCompletion);
|
|
336
415
|
|
|
337
416
|
// Validate output — reject empty or placeholder responses
|
|
338
|
-
if (
|
|
417
|
+
if (isEmptyMeetingContent(content)) {
|
|
339
418
|
log('warn', `Meeting ${meetingId}: agent ${agentId} returned empty output for ${roundName} — rejecting`);
|
|
340
419
|
// Don't record it — agent will be re-dispatched on next tick
|
|
341
420
|
saveMeeting(meeting);
|
|
342
421
|
return;
|
|
343
422
|
}
|
|
344
|
-
const content = rawContent;
|
|
345
423
|
|
|
346
424
|
if (roundName === 'investigate') {
|
|
347
425
|
meeting.findings[agentId] = { content, submittedAt: ts() };
|
package/engine/routing.js
CHANGED
|
@@ -125,7 +125,8 @@ function normalizeWorkType(workType, fallback = WORK_TYPE.IMPLEMENT) {
|
|
|
125
125
|
|
|
126
126
|
function routeForWorkType(workType) {
|
|
127
127
|
const routes = getRoutingTableCached();
|
|
128
|
-
|
|
128
|
+
const routeKey = normalizeWorkType(workType).toLowerCase();
|
|
129
|
+
return routes[routeKey] || routes[WORK_TYPE.IMPLEMENT] || { preferred: ANY_AGENT, fallback: ANY_AGENT };
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
function isAgentHardPinned(item) {
|
package/engine/shared.js
CHANGED
|
@@ -445,12 +445,15 @@ function mutateCooldowns(mutator) {
|
|
|
445
445
|
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
let _uidCounter = 0;
|
|
449
|
+
|
|
448
450
|
/**
|
|
449
|
-
* Generate a unique ID suffix: timestamp +
|
|
451
|
+
* Generate a unique ID suffix: timestamp + monotonic counter + random chars.
|
|
450
452
|
* Use for filenames that could collide (dispatch IDs, temp files, etc.)
|
|
451
453
|
*/
|
|
452
454
|
function uid() {
|
|
453
|
-
|
|
455
|
+
_uidCounter = (_uidCounter + 1) % 0x1000000;
|
|
456
|
+
return Date.now().toString(36) + _uidCounter.toString(36).padStart(4, '0') + crypto.randomBytes(2).toString('hex');
|
|
454
457
|
}
|
|
455
458
|
|
|
456
459
|
/**
|
package/engine/spawn-agent.js
CHANGED
|
@@ -122,6 +122,12 @@ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, op
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function normalizeRuntimeExit(code, signal) {
|
|
126
|
+
if (Number.isInteger(code)) return code;
|
|
127
|
+
if (signal) return 128;
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
// ─── Main script execution ──────────────────────────────────────────────────
|
|
126
132
|
|
|
127
133
|
function _installHint(name, runtime) {
|
|
@@ -278,12 +284,13 @@ function main() {
|
|
|
278
284
|
}, MCP_STARTUP_TIMEOUT);
|
|
279
285
|
proc.stdout.once('data', () => { gotFirstOutput = true; clearTimeout(startupTimer); });
|
|
280
286
|
|
|
281
|
-
proc.on('close', (code) => {
|
|
287
|
+
proc.on('close', (code, signal) => {
|
|
282
288
|
clearTimeout(startupTimer);
|
|
289
|
+
const exitCode = normalizeRuntimeExit(code, signal);
|
|
283
290
|
// Write process-exit sentinel to stdout so the engine can detect completion (#716).
|
|
284
|
-
try { process.stdout.write(`\n[process-exit] code=${
|
|
285
|
-
fs.appendFileSync(debugPath, `EXIT: code=${
|
|
286
|
-
process.exit(
|
|
291
|
+
try { process.stdout.write(`\n[process-exit] code=${exitCode}${signal ? ` signal=${signal}` : ''}\n`); } catch { /* stdout may be closed */ }
|
|
292
|
+
fs.appendFileSync(debugPath, `EXIT: code=${exitCode}${signal ? ` signal=${signal}` : ''}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
|
|
293
|
+
process.exit(exitCode);
|
|
287
294
|
});
|
|
288
295
|
proc.on('error', (err) => {
|
|
289
296
|
fs.appendFileSync(debugPath, `ERROR: ${err.message}\n`);
|
|
@@ -291,6 +298,6 @@ function main() {
|
|
|
291
298
|
});
|
|
292
299
|
}
|
|
293
300
|
|
|
294
|
-
module.exports = { parseSpawnArgs, buildSpawnInvocation };
|
|
301
|
+
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit };
|
|
295
302
|
|
|
296
303
|
if (require.main === module) main();
|
package/engine.js
CHANGED
|
@@ -3543,7 +3543,7 @@ async function discoverWork(config) {
|
|
|
3543
3543
|
);
|
|
3544
3544
|
if (hasIncompleteImplements) {
|
|
3545
3545
|
const activeCount = (getDispatch().active || []).length;
|
|
3546
|
-
const maxConcurrent = config
|
|
3546
|
+
const maxConcurrent = resolveMaxConcurrent(config);
|
|
3547
3547
|
const freeSlots = Math.max(0, maxConcurrent - activeCount);
|
|
3548
3548
|
if (freeSlots === 0) {
|
|
3549
3549
|
if (allReviews.length > 0) {
|
|
@@ -3630,6 +3630,13 @@ function isSoftFixDispatch(item) {
|
|
|
3630
3630
|
return item?.type === WORK_TYPE.FIX && !routing.isAgentHardPinned(item.meta?.item);
|
|
3631
3631
|
}
|
|
3632
3632
|
|
|
3633
|
+
function resolveMaxConcurrent(config) {
|
|
3634
|
+
const raw = config?.engine?.maxConcurrent;
|
|
3635
|
+
if (raw === undefined || raw === null || raw === '') return ENGINE_DEFAULTS.maxConcurrent;
|
|
3636
|
+
const value = Number(raw);
|
|
3637
|
+
return Number.isFinite(value) && value >= 0 ? value : ENGINE_DEFAULTS.maxConcurrent;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3633
3640
|
// ─── Main Tick ──────────────────────────────────────────────────────────────
|
|
3634
3641
|
|
|
3635
3642
|
let tickCount = 0;
|
|
@@ -3936,7 +3943,7 @@ async function tickInner() {
|
|
|
3936
3943
|
{
|
|
3937
3944
|
const dispatchPre = getDispatch();
|
|
3938
3945
|
const activeCountPre = (dispatchPre.active || []).length;
|
|
3939
|
-
const maxC = config
|
|
3946
|
+
const maxC = resolveMaxConcurrent(config);
|
|
3940
3947
|
setTempBudget(Math.max(0, maxC - activeCountPre));
|
|
3941
3948
|
}
|
|
3942
3949
|
try { pruneStalePrDispatches(config); } catch (e) { log('warn', 'prune stale PR dispatches: ' + e.message); }
|
|
@@ -3954,7 +3961,7 @@ async function tickInner() {
|
|
|
3954
3961
|
// 5. Process pending dispatches — auto-spawn agents
|
|
3955
3962
|
const dispatch = getDispatch();
|
|
3956
3963
|
const activeCount = (dispatch.active || []).length;
|
|
3957
|
-
const maxConcurrent = config
|
|
3964
|
+
const maxConcurrent = resolveMaxConcurrent(config);
|
|
3958
3965
|
|
|
3959
3966
|
const slotsAvailable = Math.max(0, maxConcurrent - activeCount);
|
|
3960
3967
|
|
|
@@ -4254,7 +4261,7 @@ module.exports = {
|
|
|
4254
4261
|
|
|
4255
4262
|
// Tick
|
|
4256
4263
|
tick,
|
|
4257
|
-
_pollIntervalMsFromTicks, _shouldRunPeriodicPhase, // exported for testing
|
|
4264
|
+
resolveMaxConcurrent, _pollIntervalMsFromTicks, _shouldRunPeriodicPhase, // exported for testing
|
|
4258
4265
|
};
|
|
4259
4266
|
|
|
4260
4267
|
// ─── Entrypoint ─────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1671",
|
|
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"
|
|
@@ -64,6 +64,18 @@ Use `status: "failed"` plus an accurate `failure_class`, `retryable`, and `needs
|
|
|
64
64
|
|
|
65
65
|
Builds, dependency installs, tests, and local servers can be quiet for long periods. Run the repo's normal CLI commands and let them finish; do not add artificial progress output, heartbeat loops, or command-specific workarounds just to keep Minions active.
|
|
66
66
|
|
|
67
|
+
## Done = pushed + local validation. Do NOT wait for remote pipelines.
|
|
68
|
+
|
|
69
|
+
Your dispatch is **done** the moment (1) your fix is pushed to the branch and (2) any local validation you ran has finished. Write the completion JSON and exit immediately. **Do not** wait for the remote PR pipeline (Android OCM PR build, Espresso CloudTest, GitHub Actions, etc.) to finish before declaring done.
|
|
70
|
+
|
|
71
|
+
This applies to **every** fix dispatch, including `build-fix` tasks. Pipeline failures route back through separate engine paths (a new `build-fix` dispatch will be queued if the remote build fails); your job ends at push.
|
|
72
|
+
|
|
73
|
+
Concretely:
|
|
74
|
+
- After `git push`, write the completion report and exit. Do not start a `monitor`/`read_powershell`/`watch` loop on the pipeline.
|
|
75
|
+
- Do not sleep or busy-wait for `mergeStatus`, `buildStatus`, or any ADO/GitHub API to flip from `running` to `passing`.
|
|
76
|
+
- If you skipped local validation, say so in the completion JSON (e.g. `tests: skipped — relying on PR pipeline`) and still exit.
|
|
77
|
+
- Holding a slot to watch a pipeline is wasted capacity; the engine has its own pipeline-monitoring path.
|
|
78
|
+
|
|
67
79
|
## Checking PR and Build Status
|
|
68
80
|
|
|
69
81
|
When asked to check build status, CI results, or review state for a PR:
|