@yemi33/minions 0.1.1758 → 0.1.1760
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/bin/minions.js +49 -19
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +57 -1
- package/package.json +1 -1
- package/playbooks/shared-rules.md +2 -0
package/CHANGELOG.md
CHANGED
package/bin/minions.js
CHANGED
|
@@ -46,6 +46,40 @@ function killByPort(port) {
|
|
|
46
46
|
} catch {}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Read the engine's recorded PID from engine/control.json. Returns null if
|
|
51
|
+
* the file is missing/corrupt or the PID isn't a positive integer.
|
|
52
|
+
*/
|
|
53
|
+
function readEnginePid(minionsHome) {
|
|
54
|
+
try {
|
|
55
|
+
const data = JSON.parse(fs.readFileSync(path.join(minionsHome, 'engine', 'control.json'), 'utf8'));
|
|
56
|
+
const pid = Number(data && data.pid);
|
|
57
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
58
|
+
} catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Force-kill a process and its descendants by PID. The /T flag on taskkill
|
|
63
|
+
* recurses into the process tree on Windows, which `process.kill()` does not.
|
|
64
|
+
* On POSIX, walk pgrep first so spawned children die before the parent.
|
|
65
|
+
*/
|
|
66
|
+
function killPidTree(pid) {
|
|
67
|
+
if (!pid || pid === process.pid) return;
|
|
68
|
+
try {
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'ignore', timeout: 5000, windowsHide: true });
|
|
71
|
+
} else {
|
|
72
|
+
try {
|
|
73
|
+
const out = execSync(`pgrep -P ${pid}`, { encoding: 'utf8', timeout: 3000 });
|
|
74
|
+
for (const c of out.split(/\r?\n/).map(s => Number(s.trim())).filter(n => Number.isInteger(n) && n > 0)) {
|
|
75
|
+
try { process.kill(c, 'SIGKILL'); } catch {}
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
49
83
|
/** Kill minions processes by command-line pattern matching (wmic on Windows, pkill on Unix). */
|
|
50
84
|
function killMinionsProcesses(patterns) {
|
|
51
85
|
try {
|
|
@@ -330,14 +364,12 @@ function init() {
|
|
|
330
364
|
printPreflight(results, { label: 'Preflight checks' });
|
|
331
365
|
} catch {}
|
|
332
366
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
367
|
+
// Update flow passes --skip-start so it can perform a single visible restart afterwards.
|
|
368
|
+
if (isUpgrade && skipStart) return;
|
|
337
369
|
|
|
338
370
|
// Auto-start on fresh install; direct force-upgrade restarts automatically.
|
|
339
371
|
if (isUpgrade) {
|
|
340
|
-
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
372
|
+
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
|
|
341
373
|
}
|
|
342
374
|
console.log(isUpgrade
|
|
343
375
|
? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
|
|
@@ -422,11 +454,6 @@ function showChangelog(fromVersion) {
|
|
|
422
454
|
console.log(` cat ${changelogPath}\n`);
|
|
423
455
|
}
|
|
424
456
|
|
|
425
|
-
function writeCommandOutput(stream, output) {
|
|
426
|
-
if (!output) return;
|
|
427
|
-
stream.write(Buffer.isBuffer(output) ? output.toString('utf8') : String(output));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
457
|
function formatPackageCliCommand(args) {
|
|
431
458
|
const initScript = path.join(PKG_ROOT, 'bin', 'minions.js');
|
|
432
459
|
return `node "${initScript}" ${args.join(' ')}`;
|
|
@@ -434,17 +461,13 @@ function formatPackageCliCommand(args) {
|
|
|
434
461
|
|
|
435
462
|
function runPackageCli(args, timeout) {
|
|
436
463
|
const initScript = path.join(PKG_ROOT, 'bin', 'minions.js');
|
|
437
|
-
|
|
464
|
+
return spawnSync(process.execPath, [initScript, ...args], {
|
|
438
465
|
cwd: process.cwd(),
|
|
439
466
|
env: { ...process.env, MINIONS_HOME },
|
|
440
|
-
|
|
467
|
+
stdio: 'inherit',
|
|
441
468
|
timeout,
|
|
442
469
|
windowsHide: true,
|
|
443
470
|
});
|
|
444
|
-
|
|
445
|
-
writeCommandOutput(process.stdout, result.stdout);
|
|
446
|
-
writeCommandOutput(process.stderr, result.stderr);
|
|
447
|
-
return result;
|
|
448
471
|
}
|
|
449
472
|
|
|
450
473
|
function runPostUpdateInit() {
|
|
@@ -615,10 +638,17 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
615
638
|
// `--cli` / `--model` flags forward to `engine.js start` so the runtime
|
|
616
639
|
// fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
|
|
617
640
|
ensureInstalled();
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
//
|
|
641
|
+
// Layered kill — each step is best-effort, layered so the next still runs if
|
|
642
|
+
// one fails. Goal: the old engine is gone before we spawn a new one, even if
|
|
643
|
+
// PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
|
|
644
|
+
const oldEnginePid = readEnginePid(MINIONS_HOME);
|
|
645
|
+
// 1. Graceful stop — short timeout so a hung engine can't block what follows.
|
|
646
|
+
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
|
|
647
|
+
// 2. Force-kill the recorded engine PID and its tree (most reliable — independent of cmdline matching).
|
|
648
|
+
killPidTree(oldEnginePid);
|
|
649
|
+
// 3. Free dashboard port (catches orphan dashboards with no recorded PID).
|
|
621
650
|
killByPort(7331);
|
|
651
|
+
// 4. Belt-and-suspenders cmdline match for anything still alive.
|
|
622
652
|
killMinionsProcesses(['engine.js', 'dashboard.js']);
|
|
623
653
|
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
|
|
624
654
|
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
package/engine/lifecycle.js
CHANGED
|
@@ -542,6 +542,16 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
542
542
|
target.completedAt = ts();
|
|
543
543
|
// Restore agent info from dispatch metadata (cleared on retry reset)
|
|
544
544
|
if (meta._agentId && !target.dispatched_to) target.dispatched_to = meta._agentId;
|
|
545
|
+
// No-op marker (set by runPostCompletionHooks when the agent honored a
|
|
546
|
+
// verify-already-shipped / no-op-fix-review skill). Surfaces the
|
|
547
|
+
// rationale on the dashboard instead of marking the WI as failed.
|
|
548
|
+
if (meta._noopReason) {
|
|
549
|
+
target._noop = true;
|
|
550
|
+
target._noopReason = meta._noopReason;
|
|
551
|
+
} else {
|
|
552
|
+
delete target._noop;
|
|
553
|
+
delete target._noopReason;
|
|
554
|
+
}
|
|
545
555
|
} else if (status === WI_STATUS.FAILED) {
|
|
546
556
|
if (reason) target.failReason = reason;
|
|
547
557
|
target.failedAt = ts();
|
|
@@ -2510,6 +2520,38 @@ function parseCompletionBoolean(value) {
|
|
|
2510
2520
|
return undefined;
|
|
2511
2521
|
}
|
|
2512
2522
|
|
|
2523
|
+
// Detect a deliberate no-op completion — the agent correctly declined to make
|
|
2524
|
+
// changes (work was already shipped, dispatch premise was wrong, self-authored
|
|
2525
|
+
// review with no actionable feedback, etc.) and should NOT be flagged as a
|
|
2526
|
+
// silent failure for missing a PR. Honored signals:
|
|
2527
|
+
// - completion.noop === true (canonical, primary)
|
|
2528
|
+
// - completion.result === 'noop' OR completion.result.type === 'noop'
|
|
2529
|
+
// Returns the no-op rationale string (for surfacing in WI metadata) when
|
|
2530
|
+
// detected, or null otherwise. The status must be a successful terminal state
|
|
2531
|
+
// — a failed/partial/in-progress completion that also claims `noop` is a
|
|
2532
|
+
// contradiction and falls through to the normal contract.
|
|
2533
|
+
function parseCompletionNoop(completion) {
|
|
2534
|
+
if (!completion || typeof completion !== 'object') return null;
|
|
2535
|
+
const explicit = parseCompletionBoolean(completion.noop);
|
|
2536
|
+
let resultIsNoop = false;
|
|
2537
|
+
if (typeof completion.result === 'string') {
|
|
2538
|
+
resultIsNoop = completion.result.trim().toLowerCase() === 'noop';
|
|
2539
|
+
} else if (completion.result && typeof completion.result === 'object') {
|
|
2540
|
+
resultIsNoop = String(completion.result.type || '').trim().toLowerCase() === 'noop';
|
|
2541
|
+
}
|
|
2542
|
+
if (explicit !== true && !resultIsNoop) return null;
|
|
2543
|
+
const status = normalizeCompletionStatus(completion.status);
|
|
2544
|
+
if (status && status !== 'success' && status !== 'done' && status !== 'complete') return null;
|
|
2545
|
+
const reason = String(
|
|
2546
|
+
completion.noopReason
|
|
2547
|
+
|| completion.noop_reason
|
|
2548
|
+
|| completion.summary
|
|
2549
|
+
|| completion.reason
|
|
2550
|
+
|| ''
|
|
2551
|
+
).trim();
|
|
2552
|
+
return reason || 'no-op completion';
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2513
2555
|
function normalizeReviewVerdict(verdict) {
|
|
2514
2556
|
const value = String(verdict || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
2515
2557
|
if (value === 'approve' || value === 'approved') return 'approved';
|
|
@@ -2867,7 +2909,17 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2867
2909
|
}
|
|
2868
2910
|
}
|
|
2869
2911
|
|
|
2870
|
-
|
|
2912
|
+
// No-op signal: agent declared the work was correctly NOT done (already
|
|
2913
|
+
// shipped, dispatch premise wrong, self-authored review, etc.). Skip the PR
|
|
2914
|
+
// attachment contract — a missing PR is intentional, not a silent failure.
|
|
2915
|
+
const noopRationale = (effectiveSuccess && meta?.item?.id && !skipDoneStatus)
|
|
2916
|
+
? parseCompletionNoop(structuredCompletion)
|
|
2917
|
+
: null;
|
|
2918
|
+
if (noopRationale) {
|
|
2919
|
+
log('info', `No-op completion for ${meta.item.id}: ${noopRationale.slice(0, 200)}`);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
2871
2923
|
completionContractFailure = await enforcePrAttachmentContract(type, meta, agentId, config, resultSummary, stdout);
|
|
2872
2924
|
if (completionContractFailure?.severity === 'hard' || completionContractFailure?.nonTerminal) {
|
|
2873
2925
|
skipDoneStatus = true;
|
|
@@ -2876,6 +2928,9 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2876
2928
|
|
|
2877
2929
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
2878
2930
|
meta._agentId = agentId;
|
|
2931
|
+
if (noopRationale) {
|
|
2932
|
+
meta._noopReason = noopRationale.slice(0, 500);
|
|
2933
|
+
}
|
|
2879
2934
|
updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
2880
2935
|
}
|
|
2881
2936
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
@@ -3238,6 +3293,7 @@ module.exports = {
|
|
|
3238
3293
|
isReviewBailout,
|
|
3239
3294
|
parseStructuredCompletion,
|
|
3240
3295
|
parseCompletionFieldSummary,
|
|
3296
|
+
parseCompletionNoop,
|
|
3241
3297
|
detectNonTerminalResultSummary,
|
|
3242
3298
|
parseCompletionReportFile,
|
|
3243
3299
|
persistCompletionReport,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1760",
|
|
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"
|
|
@@ -72,6 +72,8 @@ The engine provides a completion report path in the prompt and in `MINIONS_COMPL
|
|
|
72
72
|
|
|
73
73
|
Use `status: "failed"` plus an accurate `failure_class`, `retryable`, and `needs_rerun` when the task could not be completed. For PR reviews, set `verdict` to `approved` or `changes-requested`. Include every durable artifact you created or updated in `artifacts` (PRs, notes, plans, PRDs, important files) so the dashboard can display them. Fenced `completion` blocks are still accepted as a fallback, but the JSON report is the primary signal.
|
|
74
74
|
|
|
75
|
+
**No-op completions:** when you correctly decline to do the work — the change was already shipped on master, the dispatch premise is wrong, the flagged review comment is your own author-notes, etc. — write `status: "success"`, `pr: "N/A"`, AND add `"noop": true`. The engine treats `noop: true` as the canonical signal that no PR was expected, marks the work item done with the rationale surfaced in `_noopReason` for the dashboard, and skips the missing-PR-attachment failure. Without `noop: true`, an empty PR will still be flagged as a silent failure and auto-retried up to `maxRetries` times.
|
|
76
|
+
|
|
75
77
|
## Long-Running Commands
|
|
76
78
|
|
|
77
79
|
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.
|