@yemi33/minions 0.1.2278 → 0.1.2280
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/dashboard.js +8 -0
- package/engine/pipeline.js +2 -2
- package/engine/preflight.js +11 -7
- package/engine/restart-health.js +10 -7
- package/engine/shared.js +23 -3
- package/engine.js +3 -2
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -588,10 +588,18 @@ let _prRefVerifierOverride = null; // test seam
|
|
|
588
588
|
|
|
589
589
|
// Evict stale entries when the cache exceeds _PR_REF_VERIFY_CACHE_MAX to prevent
|
|
590
590
|
// unbounded growth. Called after every Map.set() in verifyLoosePrRefIsPr.
|
|
591
|
+
// Two-pass strategy: (1) evict TTL-expired entries; (2) if still over cap,
|
|
592
|
+
// evict the oldest entries (LRU) until the cache is back within bounds.
|
|
591
593
|
function _evictPrRefVerifyCacheIfNeeded() {
|
|
592
594
|
if (_prRefVerifyCache.size <= _PR_REF_VERIFY_CACHE_MAX) return;
|
|
595
|
+
// Pass 1: evict TTL-expired entries.
|
|
593
596
|
const cutoff = Date.now() - _PR_REF_VERIFY_TTL_MS;
|
|
594
597
|
for (const [k, v] of _prRefVerifyCache) if (v.at < cutoff) _prRefVerifyCache.delete(k);
|
|
598
|
+
// Pass 2: LRU fallback — evict oldest entries if still over cap (e.g. burst of fresh entries).
|
|
599
|
+
if (_prRefVerifyCache.size <= _PR_REF_VERIFY_CACHE_MAX) return;
|
|
600
|
+
const toEvict = _prRefVerifyCache.size - _PR_REF_VERIFY_CACHE_MAX;
|
|
601
|
+
const sorted = [..._prRefVerifyCache.entries()].sort((a, b) => a[1].at - b[1].at);
|
|
602
|
+
for (let i = 0; i < toEvict; i++) _prRefVerifyCache.delete(sorted[i][0]);
|
|
595
603
|
}
|
|
596
604
|
|
|
597
605
|
// Test seam (issue #246) — inject a fake verifier so handler tests can assert
|
package/engine/pipeline.js
CHANGED
|
@@ -1239,10 +1239,10 @@ async function discoverPipelineWork(config) {
|
|
|
1239
1239
|
module.exports = {
|
|
1240
1240
|
PIPELINES_DIR,
|
|
1241
1241
|
getPipelines, getPipeline, savePipeline, deletePipeline,
|
|
1242
|
-
getPipelineRuns, getActiveRun, startRun, updateRunStage, completeRun,
|
|
1242
|
+
getPipelineRuns, getActiveRun, startRun, updateRunStage, upsertRunStage, completeRun,
|
|
1243
1243
|
discoverPipelineWork,
|
|
1244
1244
|
evaluateCondition, // exported for testing
|
|
1245
|
-
executeTaskStage, executePlanStage, executeScheduleStage, executeApiStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1245
|
+
truncatePipelineContext, executeTaskStage, executePlanStage, executeScheduleStage, executeApiStage, executeMeetingStage, executeMergePrsStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1246
1246
|
_resolvePipelineProjects, // exported for testing
|
|
1247
1247
|
_findMeetingsInRun, _findExistingPlanForMeeting, _findExistingPrdForPlan, // exported for testing
|
|
1248
1248
|
};
|
package/engine/preflight.js
CHANGED
|
@@ -626,14 +626,18 @@ function doctor(minionsHome) {
|
|
|
626
626
|
runtimeResults.push({ name: 'Engine', ok: 'warn', message: 'not started — run: minions start (see docs/engine-restart.md)' });
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
-
// Check dashboard (try HTTP)
|
|
629
|
+
// Check dashboard (try HTTP). Read the actual bound port from the
|
|
630
|
+
// dashboard-port.json beacon so the probe works when the dashboard falls
|
|
631
|
+
// back to a non-default port after EADDRINUSE scanning.
|
|
630
632
|
const http = require('http');
|
|
633
|
+
const beaconData = shared.readDashboardPortFile(minionsHome);
|
|
634
|
+
const dashPort = (beaconData && beaconData.port) || shared.DEFAULT_DASHBOARD_PORT;
|
|
631
635
|
const dashCheck = new Promise(resolve => {
|
|
632
|
-
const req = http.get(
|
|
633
|
-
resolve({ name: 'Dashboard', ok: true, message:
|
|
636
|
+
const req = http.get(`http://localhost:${dashPort}/api/health`, { timeout: 2000 }, res => {
|
|
637
|
+
resolve({ name: 'Dashboard', ok: true, message: `running on http://localhost:${dashPort}` });
|
|
634
638
|
});
|
|
635
|
-
req.on('error', () => resolve({ name: 'Dashboard', ok: 'warn', message:
|
|
636
|
-
req.on('timeout', () => { req.destroy(); resolve({ name: 'Dashboard', ok: 'warn', message:
|
|
639
|
+
req.on('error', () => resolve({ name: 'Dashboard', ok: 'warn', message: `not reachable on :${dashPort} — run: minions dash (see docs/engine-restart.md)` }));
|
|
640
|
+
req.on('timeout', () => { req.destroy(); resolve({ name: 'Dashboard', ok: 'warn', message: `not reachable on :${dashPort} — run: minions dash (see docs/engine-restart.md)` }); });
|
|
637
641
|
});
|
|
638
642
|
|
|
639
643
|
return dashCheck.then(async dashResult => {
|
|
@@ -649,9 +653,9 @@ function doctor(minionsHome) {
|
|
|
649
653
|
runtimeResults.push({ name: 'Playbooks', ok: false, message: 'no playbooks found in playbooks/ — run: minions init --force (see docs/distribution.md)' });
|
|
650
654
|
}
|
|
651
655
|
|
|
652
|
-
// Check port
|
|
656
|
+
// Check port availability (only if dashboard isn't running)
|
|
653
657
|
if (dashResult.ok !== true) {
|
|
654
|
-
runtimeResults.push({ name:
|
|
658
|
+
runtimeResults.push({ name: `Port ${dashPort}`, ok: 'warn', message: `dashboard not running — port status unknown (see docs/engine-restart.md)` });
|
|
655
659
|
}
|
|
656
660
|
|
|
657
661
|
// Self-check the out-of-process recovery net. A registered-but-broken
|
package/engine/restart-health.js
CHANGED
|
@@ -15,7 +15,7 @@ const shared = require('./shared');
|
|
|
15
15
|
// the literal is only a fallback if shared somehow failed to load.
|
|
16
16
|
const DEFAULT_RESTART_HEALTH_TIMEOUT_MS =
|
|
17
17
|
(shared.ENGINE_DEFAULTS && shared.ENGINE_DEFAULTS.restartHealthTimeoutMs) || 60000;
|
|
18
|
-
const DEFAULT_RESTART_HEALTH_INTERVAL_MS =
|
|
18
|
+
const DEFAULT_RESTART_HEALTH_INTERVAL_MS = 100;
|
|
19
19
|
// Consecutive dead-PID reads tolerated before fail-fast. A small guard avoids
|
|
20
20
|
// a single flaky tasklist / kill(0) false-negative aborting a healthy boot.
|
|
21
21
|
const DEFAULT_DEAD_POLLS_BEFORE_FAST_FAIL = 2;
|
|
@@ -45,7 +45,7 @@ function isProcessAlive(pid) {
|
|
|
45
45
|
const out = execSync(`tasklist /FI "PID eq ${n}" /NH`, {
|
|
46
46
|
encoding: 'utf8',
|
|
47
47
|
windowsHide: true,
|
|
48
|
-
timeout:
|
|
48
|
+
timeout: 1000,
|
|
49
49
|
});
|
|
50
50
|
return new RegExp(`\\b${n}\\b`).test(out) && out.toLowerCase().includes('node');
|
|
51
51
|
}
|
|
@@ -61,8 +61,8 @@ function isPortListening(port) {
|
|
|
61
61
|
if (!Number.isInteger(n) || n <= 0) return false;
|
|
62
62
|
try {
|
|
63
63
|
if (process.platform === 'win32') {
|
|
64
|
-
const out = execSync(`netstat -ano
|
|
65
|
-
encoding: 'utf8', windowsHide: true, timeout:
|
|
64
|
+
const out = execSync(`netstat -ano | findstr ":${n} "`, {
|
|
65
|
+
encoding: 'utf8', windowsHide: true, timeout: 1000, maxBuffer: 64 * 1024, shell: true,
|
|
66
66
|
});
|
|
67
67
|
const re = new RegExp(`\\s127\\.0\\.0\\.1:${n}\\s+\\S+\\s+LISTENING`, 'i');
|
|
68
68
|
const re6 = new RegExp(`\\s\\[::1?\\]:${n}\\s+\\S+\\s+LISTENING`, 'i');
|
|
@@ -70,12 +70,12 @@ function isPortListening(port) {
|
|
|
70
70
|
}
|
|
71
71
|
try {
|
|
72
72
|
const out = execSync(`lsof -nP -iTCP:${n} -sTCP:LISTEN`, {
|
|
73
|
-
encoding: 'utf8', timeout:
|
|
73
|
+
encoding: 'utf8', timeout: 1000,
|
|
74
74
|
});
|
|
75
75
|
if (/\bLISTEN\b/i.test(out)) return true;
|
|
76
76
|
} catch {}
|
|
77
77
|
const out = execSync(`ss -ltn 'sport = :${n}' 2>/dev/null || netstat -ltn 2>/dev/null || netstat -an -p tcp 2>/dev/null`, {
|
|
78
|
-
encoding: 'utf8', timeout:
|
|
78
|
+
encoding: 'utf8', timeout: 1000, shell: true,
|
|
79
79
|
});
|
|
80
80
|
return new RegExp(`(?:[:.])${n}\\b[^\\n]*\\b(?:LISTEN|LISTENING)\\b`, 'i').test(out);
|
|
81
81
|
} catch { return false; }
|
|
@@ -152,7 +152,6 @@ async function checkRestartHealth(options = {}) {
|
|
|
152
152
|
dashboardKind = 'process';
|
|
153
153
|
const dpid = normalizePid(dashboardPid);
|
|
154
154
|
const dashAlive = dpid ? isAlive(dpid) : false;
|
|
155
|
-
const portOpen = portCheck(dashboardPort);
|
|
156
155
|
// Ownership gate (opt-in via requireBeaconOwner). "PID alive + port
|
|
157
156
|
// listening" is NOT sufficient: a stale pre-restart dashboard still holding
|
|
158
157
|
// the port satisfies the listening probe, so the verifier used to report
|
|
@@ -174,6 +173,10 @@ async function checkRestartHealth(options = {}) {
|
|
|
174
173
|
beaconPid = beacon && normalizePid(beacon.pid);
|
|
175
174
|
beaconOwned = !!(beacon && beaconPid === dpid && Number(beacon.port) === Number(dashboardPort));
|
|
176
175
|
}
|
|
176
|
+
// If the beacon already confirms this PID owns the port, skip the expensive
|
|
177
|
+
// netstat — the dashboard writes dashboard-port.json in its listen() callback,
|
|
178
|
+
// so beacon.pid === dpid IS the port-bind signal.
|
|
179
|
+
const portOpen = (options.requireBeaconOwner && beaconOwned) ? true : portCheck(dashboardPort);
|
|
177
180
|
dashboardOk = !!(dashAlive && portOpen && beaconOwned);
|
|
178
181
|
dashboardDetail = `pid=${dpid || 'none'} alive=${dashAlive ? 'yes' : 'no'} port=${dashboardPort} listening=${portOpen ? 'yes' : 'no'}`
|
|
179
182
|
+ (options.requireBeaconOwner ? ` beaconPid=${beaconPid || 'none'} owned=${beaconOwned ? 'yes' : 'no'}` : '');
|
package/engine/shared.js
CHANGED
|
@@ -975,6 +975,19 @@ function _routeJsonReadToSql(p) {
|
|
|
975
975
|
* reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
|
|
976
976
|
* stale `.backup` is actively harmful. See its JSDoc for selection guidance.
|
|
977
977
|
*/
|
|
978
|
+
|
|
979
|
+
// PL-prd-no-backup (W-mqub65ez0004b3bd) — archived PRDs under prd/archive/ are
|
|
980
|
+
// permanently removed terminal artifacts. They must NOT auto-restore from a
|
|
981
|
+
// stale `.backup` sidecar (W-mouptdh1000h9f39: archived PRD came back and
|
|
982
|
+
// re-dispatched work). Intentionally NARROW to prd/archive/ only:
|
|
983
|
+
// - prd/archive/*.json → no backup write + no safeJson restore
|
|
984
|
+
// - prd/*.json (root-level canonical) → normal backup/restore lifecycle
|
|
985
|
+
// preserved so concurrent-sweep loss is recoverable (prd-rename-race.test.js).
|
|
986
|
+
const _NO_BACKUP_JSON_RE = /(?:^|[\\/])prd[\\/]archive[\\/][^\\/]+\.json$/i;
|
|
987
|
+
function _isNoBackupJsonPath(p) {
|
|
988
|
+
return typeof p === 'string' && _NO_BACKUP_JSON_RE.test(p);
|
|
989
|
+
}
|
|
990
|
+
|
|
978
991
|
function safeJson(p) {
|
|
979
992
|
// Internal opt-out (positional second arg from mutateJsonFileLocked):
|
|
980
993
|
// when truthy, skip the SQL-routing shim and do a raw disk read. Used
|
|
@@ -1008,6 +1021,8 @@ function safeJson(p) {
|
|
|
1008
1021
|
console.error(`[safeJson] parse failure for ${path.basename(p)}: ${parseErr.message}`);
|
|
1009
1022
|
}
|
|
1010
1023
|
}
|
|
1024
|
+
// Archived PRDs (prd/archive/) are permanently gone — skip .backup restore.
|
|
1025
|
+
if (_isNoBackupJsonPath(p)) return null;
|
|
1011
1026
|
// Primary missing or corrupted — try restoring from .backup sidecar.
|
|
1012
1027
|
const backupPath = p + '.backup';
|
|
1013
1028
|
try {
|
|
@@ -1837,9 +1852,14 @@ function mutateJsonFileLocked(filePath, mutateFn, {
|
|
|
1837
1852
|
const finalData = next === undefined ? data : next;
|
|
1838
1853
|
const shouldWrite = !skipWriteIfUnchanged || parsedInvalid || JSON.stringify(finalData) !== beforeSerialized;
|
|
1839
1854
|
if (shouldWrite) {
|
|
1840
|
-
// Back up last-known-good state before mutation (best-effort)
|
|
1841
|
-
|
|
1842
|
-
|
|
1855
|
+
// Back up last-known-good state before mutation (best-effort). SKIP for
|
|
1856
|
+
// archived PRDs under prd/archive/: a .backup there is resurrection fuel
|
|
1857
|
+
// (PL-prd-no-backup, W-mqub65ez0004b3bd). Root-level prd/*.json retains
|
|
1858
|
+
// the backup lifecycle — concurrent-sweep loss must be recoverable.
|
|
1859
|
+
if (!_isNoBackupJsonPath(filePath)) {
|
|
1860
|
+
const backupPath = filePath + '.backup';
|
|
1861
|
+
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1862
|
+
}
|
|
1843
1863
|
safeWrite(filePath, finalData);
|
|
1844
1864
|
// Side-effect hook fired only when an actual write happened. Callers
|
|
1845
1865
|
// use this to emit cache-invalidation signals (events table row) so
|
package/engine.js
CHANGED
|
@@ -6121,8 +6121,9 @@ function materializePlansAsWorkItems(config) {
|
|
|
6121
6121
|
continue; // Skip — waiting for human approval
|
|
6122
6122
|
}
|
|
6123
6123
|
}
|
|
6124
|
-
if (planStatus === PLAN_STATUS.PAUSED || planStatus === PLAN_STATUS.REJECTED ||
|
|
6125
|
-
|
|
6124
|
+
if (planStatus === PLAN_STATUS.PAUSED || planStatus === PLAN_STATUS.REJECTED ||
|
|
6125
|
+
planStatus === PLAN_STATUS.REVISION_REQUESTED || planStatus === PLAN_STATUS.COMPLETED) {
|
|
6126
|
+
continue; // Skip — paused, rejected, revision-requested, or completed
|
|
6126
6127
|
}
|
|
6127
6128
|
// Stale PRDs: source plan was revised — don't materialize NEW items until user regenerates
|
|
6128
6129
|
if (plan.planStale) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2280",
|
|
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"
|