@yemi33/minions 0.1.1906 → 0.1.1908
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/bin/minions.js +19 -3
- package/engine/lifecycle.js +119 -15
- package/engine/restart-health.js +1 -1
- package/package.json +1 -1
- package/engine/copilot-models.json +0 -5
package/bin/minions.js
CHANGED
|
@@ -168,12 +168,26 @@ function killMinionsProcesses(patterns) {
|
|
|
168
168
|
} catch {}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/** Open append-mode FDs under engine/ for detached-process stdio. If the file
|
|
172
|
+
* can't be opened we fall back to 'ignore' rather than blocking the restart. */
|
|
173
|
+
function _openStdioLog(name) {
|
|
174
|
+
try {
|
|
175
|
+
const dir = path.join(MINIONS_HOME, 'engine');
|
|
176
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
177
|
+
return fs.openSync(path.join(dir, name), 'a');
|
|
178
|
+
} catch { return 'ignore'; }
|
|
179
|
+
}
|
|
180
|
+
|
|
171
181
|
/** Spawn a detached dashboard with self-open suppressed — the CLI decides
|
|
172
|
-
* when to open a browser based on whether a real tab reconnects post-health.
|
|
182
|
+
* when to open a browser based on whether a real tab reconnects post-health.
|
|
183
|
+
* stdout/stderr land in engine/dashboard-stdio.log so silent startup crashes
|
|
184
|
+
* leave a postmortem instead of vanishing. */
|
|
173
185
|
function spawnDashboard() {
|
|
174
186
|
const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
|
|
187
|
+
const out = _openStdioLog('dashboard-stdio.log');
|
|
188
|
+
const err = _openStdioLog('dashboard-stdio.log');
|
|
175
189
|
const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
176
|
-
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true, env
|
|
190
|
+
cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true, env
|
|
177
191
|
});
|
|
178
192
|
proc.unref();
|
|
179
193
|
return proc;
|
|
@@ -736,8 +750,10 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
736
750
|
// Clear stale beacons AFTER the kill so the old dashboard's last writes
|
|
737
751
|
// can't repopulate the file in the gap between clear and shutdown.
|
|
738
752
|
_clearDashboardBrowserState(MINIONS_HOME);
|
|
753
|
+
const engineOut = _openStdioLog('engine-stdio.log');
|
|
754
|
+
const engineErr = _openStdioLog('engine-stdio.log');
|
|
739
755
|
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
|
|
740
|
-
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
756
|
+
cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true
|
|
741
757
|
});
|
|
742
758
|
engineProc.unref();
|
|
743
759
|
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
package/engine/lifecycle.js
CHANGED
|
@@ -2998,6 +2998,93 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
|
|
|
2998
2998
|
shared.writeToInbox(agentId || 'engine', `agent-${outcome}-${dispatchItem.id}`, content, null, metadata);
|
|
2999
2999
|
}
|
|
3000
3000
|
|
|
3001
|
+
/**
|
|
3002
|
+
* Permissively pull all assistant-message content out of a stream-json log.
|
|
3003
|
+
*
|
|
3004
|
+
* The standard runtime parsers (engine/runtimes/*.parseOutput) intentionally drop
|
|
3005
|
+
* `assistant.message` content when the same event also carries `toolRequests`
|
|
3006
|
+
* (so a "I'll create files now" preamble doesn't leak into the user-visible
|
|
3007
|
+
* result text). For decompose dispatches the JSON block IS the deliverable, so
|
|
3008
|
+
* if the agent emits the block in the same turn it issues a tool call (very
|
|
3009
|
+
* common — the agent writes a sidecar note in the same turn) the standard
|
|
3010
|
+
* parser drops the entire JSON and decomposition silently produces no children
|
|
3011
|
+
* (W-mp3d2e6u000i9ca9). This helper concatenates EVERY content/deltaContent
|
|
3012
|
+
* chunk regardless of toolRequests so the JSON-block regex can find it.
|
|
3013
|
+
*
|
|
3014
|
+
* Returns '' when raw is empty or contains no parseable assistant content.
|
|
3015
|
+
*/
|
|
3016
|
+
function _collectAllAssistantContent(raw) {
|
|
3017
|
+
const safeRaw = raw == null ? '' : String(raw);
|
|
3018
|
+
if (!safeRaw) return '';
|
|
3019
|
+
const parts = [];
|
|
3020
|
+
for (const rawLine of safeRaw.split('\n')) {
|
|
3021
|
+
const line = rawLine.trim();
|
|
3022
|
+
if (!line || !line.startsWith('{')) continue;
|
|
3023
|
+
let obj;
|
|
3024
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
3025
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
3026
|
+
const type = obj.type;
|
|
3027
|
+
// Copilot stream-json: {type:'assistant.message',data:{content,toolRequests}}
|
|
3028
|
+
if (type === 'assistant.message' || type === 'assistant.message_delta') {
|
|
3029
|
+
const c = obj.data?.content;
|
|
3030
|
+
if (typeof c === 'string' && c) parts.push(c);
|
|
3031
|
+
const d = obj.data?.deltaContent;
|
|
3032
|
+
if (typeof d === 'string' && d) parts.push(d);
|
|
3033
|
+
}
|
|
3034
|
+
// Claude stream-json: {type:'assistant',message:{content:[{type:'text',text}]}}
|
|
3035
|
+
if (type === 'assistant' && obj.message?.content) {
|
|
3036
|
+
const blocks = Array.isArray(obj.message.content) ? obj.message.content : [];
|
|
3037
|
+
for (const b of blocks) {
|
|
3038
|
+
if (b?.type === 'text' && typeof b.text === 'string' && b.text) parts.push(b.text);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return parts.join('');
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
/**
|
|
3046
|
+
* Extract a decomposition object ({parent_id, sub_items}) from raw agent stdout.
|
|
3047
|
+
*
|
|
3048
|
+
* Strategy:
|
|
3049
|
+
* 1. Try the standard runtime parser first (preserves existing behavior when
|
|
3050
|
+
* the agent emits the block in a content-only turn).
|
|
3051
|
+
* 2. Fall back to a permissive scan that joins ALL assistant content. This
|
|
3052
|
+
* catches the common case where the agent emits the JSON block in the
|
|
3053
|
+
* same turn as a tool call (which the standard parser drops).
|
|
3054
|
+
*
|
|
3055
|
+
* In both passes, scan ALL ```json fenced blocks and pick the first one whose
|
|
3056
|
+
* parsed body has a non-empty `sub_items` (or `subItems`) array. This is more
|
|
3057
|
+
* robust than picking the first match — agents sometimes emit example/spec
|
|
3058
|
+
* JSON blocks before the real decomposition output.
|
|
3059
|
+
*
|
|
3060
|
+
* Returns the parsed decomposition object, or null if none found.
|
|
3061
|
+
* Exported for unit testing.
|
|
3062
|
+
*/
|
|
3063
|
+
function extractDecompositionJson(stdout, runtimeName) {
|
|
3064
|
+
const candidates = [];
|
|
3065
|
+
try {
|
|
3066
|
+
const parsed = shared.parseStreamJsonOutput(stdout, runtimeName);
|
|
3067
|
+
if (parsed?.text) candidates.push(parsed.text);
|
|
3068
|
+
} catch { /* runtime resolution failure — fall through to permissive scan */ }
|
|
3069
|
+
const permissive = _collectAllAssistantContent(stdout);
|
|
3070
|
+
if (permissive && permissive !== candidates[0]) candidates.push(permissive);
|
|
3071
|
+
|
|
3072
|
+
const blockRe = /```json\s*\n([\s\S]*?)```/g;
|
|
3073
|
+
for (const text of candidates) {
|
|
3074
|
+
if (!text) continue;
|
|
3075
|
+
blockRe.lastIndex = 0;
|
|
3076
|
+
let m;
|
|
3077
|
+
while ((m = blockRe.exec(text)) !== null) {
|
|
3078
|
+
let parsed;
|
|
3079
|
+
try { parsed = JSON.parse(m[1]); }
|
|
3080
|
+
catch { continue; }
|
|
3081
|
+
const subs = parsed?.sub_items || parsed?.subItems;
|
|
3082
|
+
if (Array.isArray(subs) && subs.length > 0) return parsed;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3001
3088
|
/**
|
|
3002
3089
|
* Handle decomposition result — parse sub-items from agent output and create child work items.
|
|
3003
3090
|
* Called from runPostCompletionHooks when type === 'decompose'.
|
|
@@ -3007,19 +3094,9 @@ function handleDecompositionResult(stdout, meta, config, runtimeName) {
|
|
|
3007
3094
|
const parentId = meta?.item?.id;
|
|
3008
3095
|
if (!parentId) return 0;
|
|
3009
3096
|
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
if (!jsonMatch) {
|
|
3014
|
-
log('warn', `Decomposition for ${parentId}: no JSON block found in output`);
|
|
3015
|
-
return 0;
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
let decomposition;
|
|
3019
|
-
try {
|
|
3020
|
-
decomposition = JSON.parse(jsonMatch[1]);
|
|
3021
|
-
} catch (err) {
|
|
3022
|
-
log('warn', `Decomposition for ${parentId}: invalid JSON — ${err.message}`);
|
|
3097
|
+
const decomposition = extractDecompositionJson(stdout, runtimeName);
|
|
3098
|
+
if (!decomposition) {
|
|
3099
|
+
log('warn', `Decomposition for ${parentId}: no usable JSON block found in output`);
|
|
3023
3100
|
return 0;
|
|
3024
3101
|
}
|
|
3025
3102
|
|
|
@@ -3171,8 +3248,33 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
3171
3248
|
let skipDoneStatus = false;
|
|
3172
3249
|
if (type === WORK_TYPE.DECOMPOSE && effectiveSuccess && meta?.item?.id) {
|
|
3173
3250
|
const subCount = handleDecompositionResult(stdout, meta, config, runtimeName);
|
|
3174
|
-
if (subCount > 0)
|
|
3175
|
-
|
|
3251
|
+
if (subCount > 0) {
|
|
3252
|
+
skipDoneStatus = true; // parent already marked 'decomposed' by handler
|
|
3253
|
+
} else {
|
|
3254
|
+
// Decompose claimed success but produced no children. Marking the parent
|
|
3255
|
+
// DONE here would silently terminate an implement:large item with zero
|
|
3256
|
+
// PRs (W-mp3d2e6u000i9ca9). Instead: clear _decomposing so the next
|
|
3257
|
+
// discoverFromWorkItems pass can re-enter the decompose flow, and skip
|
|
3258
|
+
// the DONE update so the parent stays pending. The dispatch retry layer
|
|
3259
|
+
// will requeue it through the standard cooldown/backoff path.
|
|
3260
|
+
skipDoneStatus = true;
|
|
3261
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
3262
|
+
if (wiPath) {
|
|
3263
|
+
try {
|
|
3264
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
3265
|
+
if (!Array.isArray(data)) return data;
|
|
3266
|
+
const wi = data.find(i => i.id === meta.item.id);
|
|
3267
|
+
if (wi) {
|
|
3268
|
+
delete wi._decomposing;
|
|
3269
|
+
wi._lastDecomposeFailure = ts();
|
|
3270
|
+
wi._pendingReason = 'decompose_no_children';
|
|
3271
|
+
}
|
|
3272
|
+
return data;
|
|
3273
|
+
}, { skipWriteIfUnchanged: true });
|
|
3274
|
+
} catch (err) { log('warn', `Decompose retry-prep cleanup: ${err.message}`); }
|
|
3275
|
+
}
|
|
3276
|
+
log('warn', `Decomposition for ${meta.item.id}: success report but no children created — parent left pending for retry`);
|
|
3277
|
+
}
|
|
3176
3278
|
}
|
|
3177
3279
|
|
|
3178
3280
|
// Verify review work items include a verdict — must run BEFORE updateWorkItemStatus(DONE),
|
|
@@ -3764,4 +3866,6 @@ module.exports = {
|
|
|
3764
3866
|
processPendingRebases,
|
|
3765
3867
|
findDependentActivePrs,
|
|
3766
3868
|
isPrAttachmentRequired,
|
|
3869
|
+
extractDecompositionJson,
|
|
3870
|
+
handleDecompositionResult,
|
|
3767
3871
|
};
|
package/engine/restart-health.js
CHANGED
|
@@ -92,7 +92,7 @@ async function checkRestartHealth(options = {}) {
|
|
|
92
92
|
const engineAlive = pid ? isAlive(pid) : false;
|
|
93
93
|
const engineOk = control && control.state === 'running' && engineAlive;
|
|
94
94
|
|
|
95
|
-
const dashboard = await getJson(dashboardUrl,
|
|
95
|
+
const dashboard = await getJson(dashboardUrl, 3000);
|
|
96
96
|
const dashboardStatus = dashboard && dashboard.json && dashboard.json.status;
|
|
97
97
|
const dashboardOk = !!(dashboard && dashboard.ok && dashboardStatus === 'healthy');
|
|
98
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1908",
|
|
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"
|