@yemi33/minions 0.1.2215 → 0.1.2217
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 +22 -0
- package/dashboard/js/render-other.js +6 -6
- package/dashboard/js/settings.js +25 -22
- package/dashboard.js +37 -26
- package/docs/README.md +2 -1
- package/docs/deprecated.json +12 -0
- package/docs/harness-propagation.md +3 -3
- package/docs/live-checkout-mode.md +14 -12
- package/docs/specs/agent-configurability.md +271 -0
- package/engine/cleanup.js +1 -1
- package/engine/cli.js +8 -2
- package/engine/consolidation.js +1 -1
- package/engine/lifecycle.js +1 -1
- package/engine/pipeline.js +48 -5
- package/engine/pr-clone-keep.js +1 -1
- package/engine/preflight.js +38 -13
- package/engine/project-discovery.js +8 -7
- package/engine/projects.js +1 -1
- package/engine/shared.js +111 -19
- package/engine/timeout.js +1 -1
- package/engine/worktree-pool.js +1 -1
- package/engine.js +7 -7
- package/package.json +1 -1
package/engine/shared.js
CHANGED
|
@@ -192,6 +192,54 @@ function ts() { return new Date().toISOString(); }
|
|
|
192
192
|
function logTs() { return new Date().toLocaleTimeString(); }
|
|
193
193
|
function dateStamp() { return new Date().toISOString().slice(0, 10); }
|
|
194
194
|
|
|
195
|
+
// ── Node / node:sqlite version gate (issue #244) ────────────────────────────
|
|
196
|
+
// `node:sqlite` (the only state backend post Phase 9.4) is built-in only on
|
|
197
|
+
// Node >= 22.5.0. On older Node the `--experimental-sqlite` self-reexec is a
|
|
198
|
+
// no-op and every getDb() throws a raw stack trace. These helpers give the CLI
|
|
199
|
+
// preflight + `minions doctor` a single, NUMERIC (not lexical) version gate and
|
|
200
|
+
// one canonical remediation line so both surfaces agree.
|
|
201
|
+
const NODE_SQLITE_MIN_VERSION = '22.5.0';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Numeric semver-ish comparison of two dotted version strings. Compares
|
|
205
|
+
* major/minor/patch as integers so '22.5.0' sorts AFTER '22.10.0' would NOT —
|
|
206
|
+
* i.e. it does NOT lexically compare ('9' > '22' under string compare). Missing
|
|
207
|
+
* components are treated as 0. Returns -1 (a<b), 0 (a==b), or 1 (a>b).
|
|
208
|
+
*/
|
|
209
|
+
function compareDottedVersions(a, b) {
|
|
210
|
+
const parse = (v) => String(v == null ? '' : v)
|
|
211
|
+
.split('.')
|
|
212
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
213
|
+
const av = parse(a);
|
|
214
|
+
const bv = parse(b);
|
|
215
|
+
const len = Math.max(av.length, bv.length);
|
|
216
|
+
for (let i = 0; i < len; i++) {
|
|
217
|
+
const x = av[i] || 0;
|
|
218
|
+
const y = bv[i] || 0;
|
|
219
|
+
if (x > y) return 1;
|
|
220
|
+
if (x < y) return -1;
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* True when the given Node version (default: the running runtime) can load the
|
|
227
|
+
* built-in `node:sqlite` module — i.e. Node >= 22.5.0. Numeric comparison, so
|
|
228
|
+
* the 22.4.x → 22.5.0 boundary is handled correctly and 22.10+ / 24+ pass.
|
|
229
|
+
*/
|
|
230
|
+
function nodeSupportsBuiltinSqlite(version = process.versions.node) {
|
|
231
|
+
return compareDottedVersions(version, NODE_SQLITE_MIN_VERSION) >= 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The canonical one-line remediation emitted by both `minions doctor` (as the
|
|
236
|
+
* failing Node.js check message) and the top-level CLI preflight when Node is
|
|
237
|
+
* too old for built-in node:sqlite. Keep the wording identical across surfaces.
|
|
238
|
+
*/
|
|
239
|
+
function nodeSqliteRemediationLine(version = process.versions.node) {
|
|
240
|
+
return `Node.js: v${version} - Minions requires Node >= 22.5 (24 LTS recommended) for built-in node:sqlite. Upgrade Node, then run minions start.`;
|
|
241
|
+
}
|
|
242
|
+
|
|
195
243
|
// ── F6 (P-f6commentedit): Comment-edit dedup helpers ────────────────────────
|
|
196
244
|
// Shared by engine/github.js + engine/ado.js pollPrHumanComments so a comment
|
|
197
245
|
// EDITED after first observation triggers a single re-dispatch (and only one).
|
|
@@ -2483,25 +2531,60 @@ function classifyInboxItem(name, content) {
|
|
|
2483
2531
|
return 'project-notes';
|
|
2484
2532
|
}
|
|
2485
2533
|
|
|
2486
|
-
// ──
|
|
2487
|
-
// Per-project switch between the default `
|
|
2488
|
-
// dedicated worktree per dispatch under ../worktrees) and `live` mode (the
|
|
2534
|
+
// ── Checkout Mode Enum (P-a3f9b201; consolidated W-mqiaw974, issue #241) ──────
|
|
2535
|
+
// Per-project switch between the default `worktree` mode (engine creates a
|
|
2536
|
+
// dedicated git worktree per dispatch under ../worktrees) and `live` mode (the
|
|
2489
2537
|
// agent runs directly in the operator's working checkout — used for repos
|
|
2490
2538
|
// where git worktrees are unworkable, e.g. submodules, hooks, large binary
|
|
2491
|
-
// caches). Default is `
|
|
2492
|
-
// project entry MUST read as '
|
|
2539
|
+
// caches). Default is `worktree`; absent/undefined `checkoutMode` on a
|
|
2540
|
+
// project entry MUST read as 'worktree' everywhere downstream. Unknown
|
|
2493
2541
|
// values are rejected at the validators — never silently coerced — so a
|
|
2494
2542
|
// typo in the dashboard or in config.json cannot wedge dispatch into an
|
|
2495
2543
|
// unknown mode.
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2544
|
+
//
|
|
2545
|
+
// LEGACY FIELD (back-compat): this field used to be named `worktreeMode` with
|
|
2546
|
+
// the enum { isolated, live }. The rename consolidated the two overlapping
|
|
2547
|
+
// fields into a single `checkoutMode` { worktree, live }: `isolated` → the
|
|
2548
|
+
// implicit `worktree` behavior, `live` → `live`. `resolveCheckoutMode` still
|
|
2549
|
+
// reads the legacy `worktreeMode` field so existing live-checkout projects in
|
|
2550
|
+
// config.json keep working without an on-disk rewrite. Tracked in
|
|
2551
|
+
// docs/deprecated.json (id: worktreemode-field-rename).
|
|
2552
|
+
const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
2553
|
+
|
|
2554
|
+
// Resolve a project's effective checkout mode, honoring the legacy
|
|
2555
|
+
// `worktreeMode` field for back-compat. Reading order:
|
|
2556
|
+
// 1. canonical `project.checkoutMode` ('worktree' | 'live')
|
|
2557
|
+
// 2. legacy `project.worktreeMode` ('isolated' → 'worktree', 'live' → 'live')
|
|
2558
|
+
// 3. default → 'worktree'
|
|
2559
|
+
// Always returns one of CHECKOUT_MODES — never undefined — so call sites can
|
|
2560
|
+
// compare against the enum without a falsy guard.
|
|
2561
|
+
function resolveCheckoutMode(project) {
|
|
2562
|
+
if (!project || typeof project !== 'object') return CHECKOUT_MODES.WORKTREE;
|
|
2563
|
+
const canonical = project.checkoutMode;
|
|
2564
|
+
if (canonical === CHECKOUT_MODES.LIVE) return CHECKOUT_MODES.LIVE;
|
|
2565
|
+
if (canonical === CHECKOUT_MODES.WORKTREE) return CHECKOUT_MODES.WORKTREE;
|
|
2566
|
+
// Legacy field fallback (only consulted when checkoutMode is absent/unknown).
|
|
2567
|
+
const legacy = project.worktreeMode;
|
|
2568
|
+
if (legacy === 'live') return CHECKOUT_MODES.LIVE;
|
|
2569
|
+
// legacy 'isolated' (and anything else) → the default worktree behavior.
|
|
2570
|
+
return CHECKOUT_MODES.WORKTREE;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
// Convenience predicate: does this project dispatch in-place (live checkout)?
|
|
2574
|
+
function isLiveCheckoutProject(project) {
|
|
2575
|
+
return resolveCheckoutMode(project) === CHECKOUT_MODES.LIVE;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
function validateCheckoutMode(value) {
|
|
2499
2579
|
if (value === undefined || value === null || value === '') return undefined;
|
|
2500
2580
|
if (typeof value !== 'string') {
|
|
2501
|
-
throw _httpError(400, `Invalid
|
|
2581
|
+
throw _httpError(400, `Invalid checkoutMode: must be a string (got ${typeof value}). Accepted values: 'worktree', 'live'.`);
|
|
2502
2582
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2583
|
+
// Back-compat: silently coerce the legacy 'isolated' value to the canonical
|
|
2584
|
+
// 'worktree' so an old client / cached dashboard tab doesn't 400.
|
|
2585
|
+
if (value === 'isolated') return CHECKOUT_MODES.WORKTREE;
|
|
2586
|
+
if (value !== CHECKOUT_MODES.WORKTREE && value !== CHECKOUT_MODES.LIVE) {
|
|
2587
|
+
throw _httpError(400, `Invalid checkoutMode: "${value}". Accepted values: 'worktree' (default), 'live'.`);
|
|
2505
2588
|
}
|
|
2506
2589
|
return value;
|
|
2507
2590
|
}
|
|
@@ -5564,7 +5647,9 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
5564
5647
|
* field for read-only stages. Only code-mutating pipeline stages need a
|
|
5565
5648
|
* worktree, and they take the normal code-mutating path below.
|
|
5566
5649
|
*
|
|
5567
|
-
* **Live-checkout mode (P-a3f9b202).** When
|
|
5650
|
+
* **Live-checkout mode (P-a3f9b202).** When the project's resolved checkout
|
|
5651
|
+
* mode is `'live'` (`shared.isLiveCheckoutProject(project)`; canonical
|
|
5652
|
+
* `project.checkoutMode === 'live'` or legacy `project.worktreeMode === 'live'`)
|
|
5568
5653
|
* the resolver short-circuits BEFORE both branches below and returns
|
|
5569
5654
|
* `{ cwd: <abs localPath>, worktreeRootDir: null, liveMode: true }` for ALL
|
|
5570
5655
|
* task types — read-only and code-mutating alike. The agent runs directly
|
|
@@ -5591,7 +5676,7 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
5591
5676
|
* caught post-resolve). The mutating containment check happens later in
|
|
5592
5677
|
* spawnAgent, against the actual worktree path.
|
|
5593
5678
|
*
|
|
5594
|
-
* @param {{ localPath?: string|null, worktreeMode?: string|null }|null|undefined} project
|
|
5679
|
+
* @param {{ localPath?: string|null, checkoutMode?: string|null, worktreeMode?: string|null }|null|undefined} project
|
|
5595
5680
|
* @param {string} type — work type (e.g. 'fix', 'explore', 'meeting')
|
|
5596
5681
|
* @param {string} minionsDir — MINIONS_DIR fallback anchor (ignored in live mode)
|
|
5597
5682
|
* @param {{ workdir?: string|null }} [options] — optional per-WI overrides
|
|
@@ -5603,7 +5688,7 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
5603
5688
|
* then runs shared.applyWorkdir(worktreePath, result.workdir) to land
|
|
5604
5689
|
* the agent inside the subpackage cwd)
|
|
5605
5690
|
* The optional `liveMode` discriminator lets callers branch on one boolean
|
|
5606
|
-
* instead of re-
|
|
5691
|
+
* instead of re-resolving the project's checkout mode.
|
|
5607
5692
|
* @throws {Error} LIVE_CHECKOUT_NO_LOCALPATH (live mode, missing localPath),
|
|
5608
5693
|
* INVALID_WORKDIR (live or read-only, workdir escapes base),
|
|
5609
5694
|
* WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT (isolated code-mutating),
|
|
@@ -5618,11 +5703,11 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
|
|
|
5618
5703
|
// Runs BEFORE the read-only / code-mutating split so live mode is the
|
|
5619
5704
|
// single decision point regardless of task type — read-only tasks in
|
|
5620
5705
|
// live mode still report liveMode:true so downstream callers don't have
|
|
5621
|
-
// to re-
|
|
5622
|
-
if (project
|
|
5706
|
+
// to re-resolve the project's checkout mode to know they're running in-place.
|
|
5707
|
+
if (isLiveCheckoutProject(project)) {
|
|
5623
5708
|
if (!project.localPath) {
|
|
5624
5709
|
const err = new Error(
|
|
5625
|
-
'live-checkout mode requires project.localPath (
|
|
5710
|
+
'live-checkout mode requires project.localPath (checkoutMode === "live" but localPath is missing/falsy).'
|
|
5626
5711
|
);
|
|
5627
5712
|
err.code = 'LIVE_CHECKOUT_NO_LOCALPATH';
|
|
5628
5713
|
throw err;
|
|
@@ -8368,6 +8453,11 @@ module.exports = {
|
|
|
8368
8453
|
EDITS_SEEN_CAP, // F6 (P-f6commentedit)
|
|
8369
8454
|
logTs,
|
|
8370
8455
|
dateStamp,
|
|
8456
|
+
// Node / node:sqlite version gate (issue #244)
|
|
8457
|
+
NODE_SQLITE_MIN_VERSION,
|
|
8458
|
+
compareDottedVersions,
|
|
8459
|
+
nodeSupportsBuiltinSqlite,
|
|
8460
|
+
nodeSqliteRemediationLine,
|
|
8371
8461
|
log,
|
|
8372
8462
|
safeRead,
|
|
8373
8463
|
safeReadOrNull,
|
|
@@ -8561,8 +8651,10 @@ module.exports = {
|
|
|
8561
8651
|
HAS_DANGEROUS_KEY_MAX_NODES,
|
|
8562
8652
|
validateProjectName,
|
|
8563
8653
|
validateProjectPath,
|
|
8564
|
-
|
|
8565
|
-
|
|
8654
|
+
CHECKOUT_MODES,
|
|
8655
|
+
validateCheckoutMode,
|
|
8656
|
+
resolveCheckoutMode,
|
|
8657
|
+
isLiveCheckoutProject,
|
|
8566
8658
|
validatePid,
|
|
8567
8659
|
PR_FIX_CAUSE,
|
|
8568
8660
|
getPrFixAutomationCause,
|
package/engine/timeout.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* engine/timeout.js — Runtime timeout, stale-orphan cleanup, steering, and idle checks.
|
|
3
3
|
*
|
|
4
|
-
* Live-checkout dispatches (project.
|
|
4
|
+
* Live-checkout dispatches (project.checkoutMode === 'live') are killed by PID
|
|
5
5
|
* exactly like isolated-mode dispatches — no special handling, no in-place
|
|
6
6
|
* `git reset`/`git clean`, no working-tree cleanup. The engine only ever sends
|
|
7
7
|
* SIGTERM/SIGKILL to the tracked process; the operator owns the checkout.
|
package/engine/worktree-pool.js
CHANGED
|
@@ -89,7 +89,7 @@ function getProjectPoolSize(projectName, config) {
|
|
|
89
89
|
// borrow path in spawnAgent into running for a project that no longer
|
|
90
90
|
// wants pooled worktrees. Beats both per-project worktreePoolSize and the
|
|
91
91
|
// engine-wide fleet default.
|
|
92
|
-
if (
|
|
92
|
+
if (shared.isLiveCheckoutProject(proj)) return 0;
|
|
93
93
|
if (proj && Number.isFinite(Number(proj.worktreePoolSize))) {
|
|
94
94
|
return Math.max(0, Math.floor(Number(proj.worktreePoolSize)));
|
|
95
95
|
}
|
package/engine.js
CHANGED
|
@@ -2046,7 +2046,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2046
2046
|
}
|
|
2047
2047
|
|
|
2048
2048
|
// ── Live-checkout mode handling (P-a3f9b204) ─────────────────────────────
|
|
2049
|
-
// When project
|
|
2049
|
+
// When the project's checkout mode is 'live', resolveSpawnPaths returned
|
|
2050
2050
|
// cwd = project.localPath, worktreeRootDir = null, liveMode = true.
|
|
2051
2051
|
// Instead of creating an engine-managed worktree we run prepareLiveCheckout
|
|
2052
2052
|
// in-place: it validates a clean tree and then checks out (or creates) the
|
|
@@ -9247,13 +9247,13 @@ async function tickInner() {
|
|
|
9247
9247
|
if (d.meta?.branch) lockedBranches.add(sanitizeBranch(d.meta.branch));
|
|
9248
9248
|
}
|
|
9249
9249
|
// P-a3f9b205: Per-project mutating-concurrency gate for live-mode projects.
|
|
9250
|
-
// When project
|
|
9251
|
-
// operator's localPath instead of a dedicated worktree, so two concurrent
|
|
9250
|
+
// When the project's checkout mode is 'live', the agent runs in-place inside
|
|
9251
|
+
// the operator's localPath instead of a dedicated worktree, so two concurrent
|
|
9252
9252
|
// mutating dispatches to the same project would clobber each other's
|
|
9253
9253
|
// index / working tree. Seed `liveProjectsInUse` from dispatch.active so
|
|
9254
9254
|
// the gate survives across ticks (not just within one allocation pass).
|
|
9255
9255
|
// Read-only types (meeting/ask/explore/plan/plan-to-prd) never write, so
|
|
9256
|
-
// they are excluded from the cap.
|
|
9256
|
+
// they are excluded from the cap. Worktree-mode projects are also
|
|
9257
9257
|
// excluded — they get a fresh worktree per dispatch and are naturally
|
|
9258
9258
|
// safe.
|
|
9259
9259
|
const liveProjectsInUse = new Set();
|
|
@@ -9262,7 +9262,7 @@ async function tickInner() {
|
|
|
9262
9262
|
const projName = d.project || d.meta?.project?.name || null;
|
|
9263
9263
|
if (!projName) continue;
|
|
9264
9264
|
const projCfg = shared.findProjectByName(shared.getProjects(config), projName);
|
|
9265
|
-
if (
|
|
9265
|
+
if (shared.isLiveCheckoutProject(projCfg)) {
|
|
9266
9266
|
liveProjectsInUse.add(projName);
|
|
9267
9267
|
}
|
|
9268
9268
|
}
|
|
@@ -9439,7 +9439,7 @@ async function tickInner() {
|
|
|
9439
9439
|
&& !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
|
|
9440
9440
|
) {
|
|
9441
9441
|
const projCfg = shared.findProjectByName(shared.getProjects(config), itemProjName);
|
|
9442
|
-
if (
|
|
9442
|
+
if (shared.isLiveCheckoutProject(projCfg)) {
|
|
9443
9443
|
liveProjectsInUse.add(itemProjName);
|
|
9444
9444
|
}
|
|
9445
9445
|
}
|
|
@@ -9517,7 +9517,7 @@ async function tickInner() {
|
|
|
9517
9517
|
const projName = d.project || d.meta?.project?.name || null;
|
|
9518
9518
|
if (!projName) continue;
|
|
9519
9519
|
const projCfg = shared.findProjectByName(shared.getProjects(config), projName);
|
|
9520
|
-
if (
|
|
9520
|
+
if (shared.isLiveCheckoutProject(projCfg)) {
|
|
9521
9521
|
postLiveProjectsInUse.add(projName);
|
|
9522
9522
|
}
|
|
9523
9523
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2217",
|
|
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"
|