@yemi33/minions 0.1.2215 → 0.1.2216
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/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/bin/minions.js
CHANGED
|
@@ -649,6 +649,28 @@ const { cmd, rest, devMode, devPort } = (() => {
|
|
|
649
649
|
const [firstCmd, ...restArgs] = out;
|
|
650
650
|
return { cmd: firstCmd, rest: restArgs, devMode: dev, devPort: port || DEFAULT_DEV_DASH_PORT };
|
|
651
651
|
})();
|
|
652
|
+
|
|
653
|
+
// ── Node / node:sqlite version gate (issue #244) ────────────────────────────
|
|
654
|
+
// node:sqlite is the only state backend post Phase 9.4 and exists only on
|
|
655
|
+
// Node >= 22.5; the --experimental-sqlite self-reexec at the top of this file
|
|
656
|
+
// is a no-op below 22.5. Without this gate, any command that touches getDb()
|
|
657
|
+
// (`minions status`, `queue`, engine delegations, …) dumps a raw node:sqlite
|
|
658
|
+
// stack trace. Fail closed with the canonical one-line remediation. Numeric
|
|
659
|
+
// version compare via shared helper — never string compares, never throws.
|
|
660
|
+
//
|
|
661
|
+
// Exempt the commands a stranded user still needs: `doctor` re-emits the same
|
|
662
|
+
// remediation as a structured FAIL (and suppresses the downstream fallout
|
|
663
|
+
// warnings); help/version are pure; uninstall/nuke must stay reachable to
|
|
664
|
+
// clean up. Everything else short-circuits before we spawn a child that would
|
|
665
|
+
// only crash with the same trace.
|
|
666
|
+
const NODE_GATE_EXEMPT = new Set([
|
|
667
|
+
'doctor', 'version', '--version', '-v', 'help', '--help', '-h', 'uninstall', 'nuke',
|
|
668
|
+
]);
|
|
669
|
+
if (!shared.nodeSupportsBuiltinSqlite() && cmd && !NODE_GATE_EXEMPT.has(cmd)) {
|
|
670
|
+
console.error(`\n ${shared.nodeSqliteRemediationLine()}\n`);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
|
|
652
674
|
let force = rest.includes('--force');
|
|
653
675
|
const skipScan = rest.includes('--skip-scan');
|
|
654
676
|
const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
|
|
@@ -73,18 +73,18 @@ function _renderProjectBranch(p) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Renders a compact pill indicating the project's dispatch mode (W-mqgzcrln002613b3):
|
|
76
|
-
// "Worktrees" (
|
|
76
|
+
// "Worktrees" (worktree — default, agents run in their own git worktree) vs
|
|
77
77
|
// "Live checkout" (live — agents run in-place inside the operator's working
|
|
78
|
-
// tree). Driven by p.
|
|
79
|
-
// unset → '
|
|
78
|
+
// tree). Driven by p.checkoutMode (shared.CHECKOUT_MODES; engine defaults
|
|
79
|
+
// unset → 'worktree'). Live is the riskier/special mode (capped to one mutating
|
|
80
80
|
// dispatch per project, refuses on a dirty tree) so it gets a more prominent
|
|
81
|
-
// color than the muted
|
|
81
|
+
// color than the muted worktree pill.
|
|
82
82
|
function _renderWorktreeModePill(p) {
|
|
83
83
|
if (!p) return '';
|
|
84
|
-
if (p.
|
|
84
|
+
if (p.checkoutMode === 'live') {
|
|
85
85
|
return ' <span class="project-mode-pill project-mode-live" title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree">⚡ Live checkout</span>';
|
|
86
86
|
}
|
|
87
|
-
return ' <span class="project-mode-pill project-mode-isolated" title="
|
|
87
|
+
return ' <span class="project-mode-pill project-mode-isolated" title="Worktree dispatch mode (default) — each agent runs in its own git worktree">Worktrees</span>';
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function _projectCachePath(project) {
|
package/dashboard/js/settings.js
CHANGED
|
@@ -259,20 +259,22 @@ async function openSettings() {
|
|
|
259
259
|
'<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
|
|
260
260
|
'</div>' +
|
|
261
261
|
'</div>';
|
|
262
|
-
// P-a3f9b207 — per-project
|
|
263
|
-
// '
|
|
264
|
-
// runs the agent directly in p.localPath for repos
|
|
265
|
-
// unworkable.
|
|
266
|
-
|
|
267
|
-
|
|
262
|
+
// P-a3f9b207 (consolidated W-mqiaw974) — per-project checkoutMode dropdown
|
|
263
|
+
// + warning chip. Default 'worktree' (engine creates a dedicated worktree
|
|
264
|
+
// per dispatch); 'live' runs the agent directly in p.localPath for repos
|
|
265
|
+
// where worktrees are unworkable. p.checkoutMode is resolved server-side
|
|
266
|
+
// (honors the legacy worktreeMode field). Chip is hidden by default and
|
|
267
|
+
// toggled reactively below.
|
|
268
|
+
var currentWtMode = (p.checkoutMode === 'live') ? 'live' : 'worktree';
|
|
269
|
+
var wtModeSearch = 'checkout mode worktree isolated live dispatch';
|
|
268
270
|
var worktreeModeBlock =
|
|
269
271
|
'<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
|
|
270
|
-
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">
|
|
271
|
-
'<select id="set-
|
|
272
|
-
'<option value="
|
|
272
|
+
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Checkout mode</label>' +
|
|
273
|
+
'<select id="set-checkoutMode-' + escHtml(p.name) + '" data-checkout-mode-select="' + escHtml(p.name) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
|
|
274
|
+
'<option value="worktree"' + (currentWtMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
|
|
273
275
|
'<option value="live"' + (currentWtMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
|
|
274
276
|
'</select>' +
|
|
275
|
-
'<div data-
|
|
277
|
+
'<div data-checkout-mode-chip="' + escHtml(p.name) + '" style="' + (currentWtMode === 'live' ? '' : 'display:none;') + 'margin-top:6px;padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4">' +
|
|
276
278
|
'⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
|
|
277
279
|
'</div>' +
|
|
278
280
|
'</div>';
|
|
@@ -666,14 +668,14 @@ async function openSettings() {
|
|
|
666
668
|
});
|
|
667
669
|
|
|
668
670
|
// P-a3f9b207 — toggle the live-mode warning chip reactively when the
|
|
669
|
-
// operator flips the per-project
|
|
670
|
-
// chip is rendered once with display:none for
|
|
671
|
+
// operator flips the per-project checkoutMode dropdown (before save). The
|
|
672
|
+
// chip is rendered once with display:none for worktree-mode projects and
|
|
671
673
|
// visible for already-live projects; this handler only flips the
|
|
672
674
|
// display style — no markup is regenerated.
|
|
673
|
-
document.querySelectorAll('[data-
|
|
675
|
+
document.querySelectorAll('[data-checkout-mode-select]').forEach(function(sel) {
|
|
674
676
|
sel.addEventListener('change', function() {
|
|
675
|
-
const projName = sel.getAttribute('data-
|
|
676
|
-
const chip = document.querySelector('[data-
|
|
677
|
+
const projName = sel.getAttribute('data-checkout-mode-select');
|
|
678
|
+
const chip = document.querySelector('[data-checkout-mode-chip="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
|
|
677
679
|
if (!chip) return;
|
|
678
680
|
chip.style.display = (sel.value === 'live') ? '' : 'none';
|
|
679
681
|
});
|
|
@@ -1083,16 +1085,17 @@ async function saveSettings() {
|
|
|
1083
1085
|
// Projects. Empty string = clear the override; the field stays optional.
|
|
1084
1086
|
const mainBranchInput = document.getElementById('set-mainBranch-' + p.name);
|
|
1085
1087
|
const mainBranchValue = mainBranchInput ? mainBranchInput.value.trim() : (p.mainBranch || '');
|
|
1086
|
-
// P-a3f9b207 — per-project
|
|
1087
|
-
// any value other than 'live' so a stale DOM
|
|
1088
|
-
// server-side validator
|
|
1089
|
-
// authoritative gate for unknown
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1088
|
+
// P-a3f9b207 (consolidated W-mqiaw974) — per-project checkoutMode.
|
|
1089
|
+
// Normalize to 'worktree' for any value other than 'live' so a stale DOM
|
|
1090
|
+
// never POSTs garbage; the server-side validator
|
|
1091
|
+
// (shared.validateCheckoutMode) is the authoritative gate for unknown
|
|
1092
|
+
// values.
|
|
1093
|
+
const wtModeInput = document.getElementById('set-checkoutMode-' + p.name);
|
|
1094
|
+
const wtModeValue = (wtModeInput && wtModeInput.value === 'live') ? 'live' : 'worktree';
|
|
1092
1095
|
return {
|
|
1093
1096
|
name: p.name,
|
|
1094
1097
|
mainBranch: mainBranchValue || null,
|
|
1095
|
-
|
|
1098
|
+
checkoutMode: wtModeValue,
|
|
1096
1099
|
workSources: {
|
|
1097
1100
|
pullRequests: { enabled: document.getElementById('set-ws-prs-' + p.name)?.checked ?? true },
|
|
1098
1101
|
workItems: { enabled: document.getElementById('set-ws-wi-' + p.name)?.checked ?? true }
|
package/dashboard.js
CHANGED
|
@@ -358,17 +358,21 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
358
358
|
} else {
|
|
359
359
|
delete currentProject.mainBranch;
|
|
360
360
|
}
|
|
361
|
-
// P-a3f9b208 — mirror
|
|
362
|
-
// candidate clears the field so the engine falls back to "
|
|
361
|
+
// P-a3f9b208 — mirror checkoutMode the same way: empty / unset on
|
|
362
|
+
// candidate clears the field so the engine falls back to "worktree".
|
|
363
363
|
// Without this branch the validated POST-body update (mutated on
|
|
364
364
|
// `candidate` in handleSettingsUpdate) is silently dropped on the way
|
|
365
365
|
// through mergeSettingsConfigUpdate → mutateDashboardConfig and never
|
|
366
366
|
// reaches disk — the endpoint would return 200 but persist nothing.
|
|
367
|
-
if (Object.prototype.hasOwnProperty.call(candidateProject, '
|
|
368
|
-
currentProject.
|
|
367
|
+
if (Object.prototype.hasOwnProperty.call(candidateProject, 'checkoutMode')) {
|
|
368
|
+
currentProject.checkoutMode = candidateProject.checkoutMode;
|
|
369
369
|
} else {
|
|
370
|
-
delete currentProject.
|
|
370
|
+
delete currentProject.checkoutMode;
|
|
371
371
|
}
|
|
372
|
+
// W-mqiaw974 (issue #241): the field was renamed from the legacy
|
|
373
|
+
// `worktreeMode`. Drop any stale legacy key on every settings save so a
|
|
374
|
+
// migrated project never carries both fields.
|
|
375
|
+
delete currentProject.worktreeMode;
|
|
372
376
|
}
|
|
373
377
|
}
|
|
374
378
|
shared.pruneDefaultClaudeConfig(current);
|
|
@@ -1206,7 +1210,7 @@ const _PROJECT_LOCAL_FOOTGUN_WARNING =
|
|
|
1206
1210
|
'git. A fresh `git worktree add` for a mutating dispatch will NOT see them, so ' +
|
|
1207
1211
|
'the dispatched agent silently underperforms. Fix: commit them, move them to ' +
|
|
1208
1212
|
'user scope (~/.claude/skills/..., ~/.copilot/skills/...), or flip the project ' +
|
|
1209
|
-
'to live-checkout mode (
|
|
1213
|
+
'to live-checkout mode (checkoutMode: live).';
|
|
1210
1214
|
|
|
1211
1215
|
function _walkUncommittedHarnessAssets(absStart, rootDir, projectName) {
|
|
1212
1216
|
const results = [];
|
|
@@ -2351,8 +2355,9 @@ function _buildStatusSlowState() {
|
|
|
2351
2355
|
branchMismatch,
|
|
2352
2356
|
// P-a3f9b209 / W-mqgzcrln002613b3 — surface the per-project dispatch
|
|
2353
2357
|
// mode so the Projects view can render a "Worktrees" vs "Live checkout"
|
|
2354
|
-
// pill.
|
|
2355
|
-
|
|
2358
|
+
// pill. resolveCheckoutMode honors the legacy worktreeMode field and
|
|
2359
|
+
// defaults to 'worktree' when unset (matches engine spawn behavior).
|
|
2360
|
+
checkoutMode: shared.resolveCheckoutMode(p),
|
|
2356
2361
|
};
|
|
2357
2362
|
}),
|
|
2358
2363
|
autoMode: {
|
|
@@ -8817,7 +8822,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8817
8822
|
confirmToken: body.confirmToken,
|
|
8818
8823
|
isValidToken: _consumeProjectConfirmToken,
|
|
8819
8824
|
name: body.name,
|
|
8820
|
-
|
|
8825
|
+
checkoutMode: body.checkoutMode ?? body.worktreeMode,
|
|
8821
8826
|
observeAuthors: Array.isArray(body.observeAuthors) ? body.observeAuthors : undefined,
|
|
8822
8827
|
});
|
|
8823
8828
|
} catch (e) {
|
|
@@ -10369,10 +10374,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10369
10374
|
name: p.name,
|
|
10370
10375
|
localPath: p.localPath || null,
|
|
10371
10376
|
mainBranch: p.mainBranch || null,
|
|
10372
|
-
// P-a3f9b207 — surface
|
|
10373
|
-
// the per-project dropdown.
|
|
10374
|
-
//
|
|
10375
|
-
|
|
10377
|
+
// P-a3f9b207 — surface checkoutMode so the Settings UI can pre-fill
|
|
10378
|
+
// the per-project dropdown. resolveCheckoutMode honors the legacy
|
|
10379
|
+
// worktreeMode field; 'worktree' (default) or 'live'.
|
|
10380
|
+
checkoutMode: shared.resolveCheckoutMode(p),
|
|
10376
10381
|
workSources: {
|
|
10377
10382
|
pullRequests: { enabled: p.workSources?.pullRequests?.enabled !== false, cooldownMinutes: p.workSources?.pullRequests?.cooldownMinutes ?? 30 },
|
|
10378
10383
|
workItems: { enabled: p.workSources?.workItems?.enabled !== false, cooldownMinutes: p.workSources?.workItems?.cooldownMinutes ?? 0 }
|
|
@@ -10755,21 +10760,27 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10755
10760
|
if (raw) proj.mainBranch = raw;
|
|
10756
10761
|
else delete proj.mainBranch;
|
|
10757
10762
|
}
|
|
10758
|
-
// P-a3f9b201 — per-project
|
|
10759
|
-
// 'live' for repos where worktrees are
|
|
10760
|
-
// null clears the override (engine falls
|
|
10761
|
-
// other value flows through
|
|
10762
|
-
// HTTP 400 on unknown values
|
|
10763
|
-
// and
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10763
|
+
// P-a3f9b201 (consolidated W-mqiaw974) — per-project checkoutMode enum
|
|
10764
|
+
// ('worktree' default, 'live' for repos where worktrees are
|
|
10765
|
+
// unworkable). Empty string / null clears the override (engine falls
|
|
10766
|
+
// back to 'worktree'); any other value flows through
|
|
10767
|
+
// shared.validateCheckoutMode which throws HTTP 400 on unknown values
|
|
10768
|
+
// (and silently coerces the legacy 'isolated' → 'worktree'). Accepts
|
|
10769
|
+
// the legacy `worktreeMode` key for back-compat. Errors propagate to
|
|
10770
|
+
// the outer catch and are returned via e.statusCode/e.message.
|
|
10771
|
+
const hasCheckoutMode = Object.prototype.hasOwnProperty.call(update, 'checkoutMode');
|
|
10772
|
+
const hasLegacyMode = Object.prototype.hasOwnProperty.call(update, 'worktreeMode');
|
|
10773
|
+
if (hasCheckoutMode || hasLegacyMode) {
|
|
10774
|
+
const raw = hasCheckoutMode ? update.checkoutMode : update.worktreeMode;
|
|
10775
|
+
if (raw === '' || raw === null || raw === undefined) {
|
|
10776
|
+
delete proj.checkoutMode;
|
|
10768
10777
|
} else {
|
|
10769
|
-
const validated = shared.
|
|
10770
|
-
if (validated === undefined) delete proj.
|
|
10771
|
-
else proj.
|
|
10778
|
+
const validated = shared.validateCheckoutMode(raw);
|
|
10779
|
+
if (validated === undefined) delete proj.checkoutMode;
|
|
10780
|
+
else proj.checkoutMode = validated;
|
|
10772
10781
|
}
|
|
10782
|
+
// Drop the legacy field so a migrated project never carries both.
|
|
10783
|
+
delete proj.worktreeMode;
|
|
10773
10784
|
}
|
|
10774
10785
|
}
|
|
10775
10786
|
}
|
package/docs/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
15
15
|
|
|
16
16
|
- [branch-derivation.md](branch-derivation.md) — Engine-side branch fallback (`work/<wi-id>`) vs. agent-authored long form, the structured-vs-loose PR-pointer extractors, and the canonical PR-fix duplication incident.
|
|
17
17
|
- [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
|
|
18
|
+
- [specs/agent-configurability.md](specs/agent-configurability.md) — Design spec for the user-configurable Role / Agent Library: Phase 1 exposes & edits each role's definition + skills; Phase 2 adds role/agent CRUD. Covers the additive `config.roles` model, resolver tiering, migration from per-agent `charter.md`, and API/UI surface.
|
|
18
19
|
- [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
|
|
19
20
|
- [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
|
|
20
21
|
- [constellation-bridge.md](constellation-bridge.md) — Read-only cross-repo bridge: `engine.constellationBridge.enabled` flag, marker-file contract, and the `minions bridge` subcommand for local debugging.
|
|
@@ -30,7 +31,7 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
30
31
|
- [harness-transparency.md](harness-transparency.md) — The `harnessUsed` self-report contract: capture (agent reports the skills / MCPs / commands / docs it used) → ground (engine cross-checks against `_harnessPropagated` and annotates `grounded:true\|false`, never dropping) → surface (PR comment, notes/inbox digest, work-item modal).
|
|
31
32
|
- [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
|
|
32
33
|
- [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
|
|
33
|
-
- [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `
|
|
34
|
+
- [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `checkoutMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
|
|
34
35
|
- [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
|
|
35
36
|
- [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
|
|
36
37
|
- [pr-auto-fix-dispatch.md](pr-auto-fix-dispatch.md) — Short reference table mapping each PR auto-fix / review dispatch site in `engine.js#discoverFromPrs` to its gate flag, plus the `pollingPaused` / `autoFixPaused` master kill-switches and the per-provider polling gates.
|
package/docs/deprecated.json
CHANGED
|
@@ -142,6 +142,18 @@
|
|
|
142
142
|
"targetRemovalDate": "2026-06-16",
|
|
143
143
|
"notes": "Unlike the three record-field aliases, nothing is written here — this is purely an input read-fallback. Removal scope (DEFERRED — gated on confirming no client still POSTs `autoObserve`, not on the expired calendar date): drop the `!body.autoObserve` fallback in the link handler in dashboard.js, drop `autoObserve?` from the route registry `params` string, and update any client (dashboard JS, ops scripts) that still POSTs `autoObserve`. After removal, callers that still send `autoObserve` will see their value silently ignored."
|
|
144
144
|
},
|
|
145
|
+
{
|
|
146
|
+
"id": "worktreemode-field-rename",
|
|
147
|
+
"description": "Legacy `project.worktreeMode` config field (enum 'isolated'|'live'). Consolidated by W-mqiaw974 (issue #241) into a single `project.checkoutMode` field (enum 'worktree'|'live'): the old 'isolated' value became the implicit default 'worktree', and the overlapping/never-shipped 'shared' value was removed. The write side is migrated — buildProjectEntry, dashboard settings POST, and projects.addProject all write `checkoutMode`, and the dashboard settings/merge paths delete any stale `worktreeMode` key on save. What survives is a READ-BRIDGE ONLY: `shared.resolveCheckoutMode(project)` (and the `isLiveCheckoutProject` predicate built on it) reads canonical `checkoutMode` first, then falls back to the legacy `worktreeMode` field ('isolated'→'worktree', 'live'→'live') so an un-migrated config.json keeps dispatching live-checkout projects correctly. `validateCheckoutMode` also silently coerces a submitted legacy 'isolated' value to 'worktree'.",
|
|
148
|
+
"code": [
|
|
149
|
+
{ "file": "engine/shared.js", "note": "resolveCheckoutMode reads project.worktreeMode as the fallback when checkoutMode is absent; validateCheckoutMode coerces 'isolated'→'worktree'. The only surviving reads of the legacy field are these two back-compat bridges." },
|
|
150
|
+
{ "file": "engine/projects.js", "note": "addProject threads options.checkoutMode ?? options.worktreeMode into buildProjectEntry (accepts the legacy options key)." },
|
|
151
|
+
{ "file": "dashboard.js", "note": "handleProjectsAdd reads body.checkoutMode ?? body.worktreeMode; handleSettingsUpdate + mergeSettingsConfigUpdate accept the legacy update.worktreeMode key and delete proj.worktreeMode on every save (active migration)." }
|
|
152
|
+
],
|
|
153
|
+
"deprecated": "2026-06-17",
|
|
154
|
+
"targetRemovalDate": "2026-09-17",
|
|
155
|
+
"notes": "Safe to remove on or after 2026-09-17 (90 days, ~3 release windows) once a sweep of every persisted config.json confirms no `worktreeMode` key remains (the dashboard settings save actively migrates each project the next time it is touched, so the residue shrinks over time) AND no external tooling reads `project.worktreeMode`. Removal scope: drop the legacy-field fallback in shared.resolveCheckoutMode, the 'isolated' coercion in validateCheckoutMode, the `?? options.worktreeMode` / `?? body.worktreeMode` input fallbacks in projects.addProject + dashboard handleProjectsAdd, the `update.worktreeMode` acceptance + `delete proj.worktreeMode` migration in dashboard handleSettingsUpdate/mergeSettingsConfigUpdate, and the legacy back-compat tests in test/unit/{worktree-mode-schema,settings-worktree-mode,live-checkout-mode}.test.js."
|
|
156
|
+
},
|
|
145
157
|
{
|
|
146
158
|
"id": "pr-observe-observe-body-param",
|
|
147
159
|
"description": "Legacy `observe` body parameter on `POST /api/pull-requests/observe`. The W-mq5s5ttx000j7ab8 endpoint sub-WI introduces canonical `contextOnly` as the inverse (`observe: false` ⇔ `contextOnly: true`) and keeps `observe` accepted for backward compat. Registering the deprecation here so the alias has a documented removal path; the WI explicitly notes this entry is the implementer's call (it is kept for backward compat and may live longer than the underscore-prefixed record fields).",
|
|
@@ -61,7 +61,7 @@ cwd**. Project-scope skills like `<repo>/.claude/skills/foo/SKILL.md` are
|
|
|
61
61
|
loaded by the CLI only if `<cwd>/.claude/skills/foo/SKILL.md` exists at
|
|
62
62
|
spawn time. See *The worktree-uncommitted footgun* below.
|
|
63
63
|
|
|
64
|
-
Live-checkout mode (`project.
|
|
64
|
+
Live-checkout mode (`project.checkoutMode: 'live'`) collapses both branches
|
|
65
65
|
to `cwd = project.localPath` for every dispatch type, so the agent sees
|
|
66
66
|
the operator's working tree as-is (including uncommitted assets). The
|
|
67
67
|
tradeoff is single-mutating-dispatch concurrency per project — see
|
|
@@ -149,7 +149,7 @@ so the only workarounds today are:
|
|
|
149
149
|
- **Move it user-scope.** Drop it under `~/.claude/skills/bar/` instead —
|
|
150
150
|
the CLI's user-skill discovery still works inside a worktree because
|
|
151
151
|
`--add-dir` attaches `~/.claude`.
|
|
152
|
-
- **Flip to live-checkout mode.** Set `project.
|
|
152
|
+
- **Flip to live-checkout mode.** Set `project.checkoutMode = 'live'` on
|
|
153
153
|
the project so every dispatch runs in `project.localPath`. Caveats in
|
|
154
154
|
`docs/live-checkout-mode.md`.
|
|
155
155
|
|
|
@@ -263,7 +263,7 @@ hand; `collectSkillFiles` / `collectCommandFiles` already do.
|
|
|
263
263
|
## Related docs
|
|
264
264
|
|
|
265
265
|
- `docs/runtime-adapters.md` — full adapter interface table.
|
|
266
|
-
- `docs/live-checkout-mode.md` — `project.
|
|
266
|
+
- `docs/live-checkout-mode.md` — `project.checkoutMode: 'live'`
|
|
267
267
|
contract (alternative to the worktree-add-dir story for repos where
|
|
268
268
|
worktrees are unworkable).
|
|
269
269
|
- `docs/skills.md` — skill block format and auto-extraction targets.
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# Live-checkout dispatch mode
|
|
2
2
|
|
|
3
|
-
> Per-project opt-in (`project.
|
|
3
|
+
> Per-project opt-in (`project.checkoutMode: 'live'`) that runs Minions agents directly inside the operator's project checkout instead of an engine-managed git worktree.
|
|
4
4
|
>
|
|
5
|
-
> Plan: `plan-w-mq5rmtt9000a42f9-2026-06-08`. PRD: `prd/minions-opg-2026-06-10.json` (items `P-a3f9b201` … `P-a3f9b209`). Default for every project remains `'
|
|
5
|
+
> Plan: `plan-w-mq5rmtt9000a42f9-2026-06-08`. PRD: `prd/minions-opg-2026-06-10.json` (items `P-a3f9b201` … `P-a3f9b209`). Default for every project remains `'worktree'`; the rest of this doc only applies when a project flips itself to `'live'`.
|
|
6
|
+
>
|
|
7
|
+
> **Field consolidation (W-mqiaw974 / issue #241).** The dispatch-mode field was renamed from the legacy `worktreeMode` (`'isolated'`|`'live'`) to a single `checkoutMode` (`'worktree'`|`'live'`). `'isolated'` became the implicit `'worktree'` (default). `shared.resolveCheckoutMode(project)` reads the canonical `checkoutMode` first and falls back to the legacy `worktreeMode` (`'isolated'`→`'worktree'`, `'live'`→`'live'`), so existing live-checkout configs keep working with no rewrite. See `docs/deprecated.json` (id `worktreemode-field-rename`).
|
|
6
8
|
|
|
7
9
|
## Motivation
|
|
8
10
|
|
|
9
|
-
The default `
|
|
11
|
+
The default `worktree` mode spawns each dispatch inside its own git worktree under `../worktrees/`. That works for almost every repo, but it falls over when:
|
|
10
12
|
|
|
11
13
|
- **`repo`-managed multi-project trees** (Android AOSP / Office mobile / Chromium) where the manifest pins dozens of nested repos and `git worktree add` either errors out or strands sub-projects.
|
|
12
14
|
- **Submodule-heavy repos** where worktrees skip `.gitmodules` configuration or leave submodules pointing at the wrong SHA.
|
|
@@ -60,9 +62,9 @@ Pool short-circuits live in `engine.js:1368` and `engine/cleanup.js`; both gate
|
|
|
60
62
|
### Enabling live mode (dashboard)
|
|
61
63
|
|
|
62
64
|
1. Open the Minions dashboard → **Settings** → **Projects** → expand the target project.
|
|
63
|
-
2. Set **
|
|
65
|
+
2. Set **Checkout mode** → **Live checkout**. A yellow warning chip appears immediately:
|
|
64
66
|
> ⚠ Live mode: dispatches run directly in this repo's checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.
|
|
65
|
-
3. Click **Save**. The dashboard POSTs the change through `mergeSettingsConfigUpdate`; `shared.
|
|
67
|
+
3. Click **Save**. The dashboard POSTs the change through `mergeSettingsConfigUpdate`; `shared.validateCheckoutMode` rejects anything other than `'worktree'` or `'live'` with HTTP 400 (the legacy `'isolated'` value is silently coerced to `'worktree'`).
|
|
66
68
|
|
|
67
69
|
### Enabling live mode (config.json)
|
|
68
70
|
|
|
@@ -71,13 +73,13 @@ Pool short-circuits live in `engine.js:1368` and `engine/cleanup.js`; both gate
|
|
|
71
73
|
"projects": [{
|
|
72
74
|
"name": "android-aosp",
|
|
73
75
|
"localPath": "/home/yemi/aosp",
|
|
74
|
-
"
|
|
76
|
+
"checkoutMode": "live",
|
|
75
77
|
// …
|
|
76
78
|
}]
|
|
77
79
|
}
|
|
78
80
|
```
|
|
79
81
|
|
|
80
|
-
Absent / `null` / `''` reads as `'
|
|
82
|
+
Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is preferred. A legacy `"worktreeMode": "live"` is still honored (and `"worktreeMode": "isolated"` reads as `'worktree'`), but new configs should use `checkoutMode`.
|
|
81
83
|
|
|
82
84
|
### Recovering from `live_checkout_dirty` refusal
|
|
83
85
|
|
|
@@ -119,11 +121,11 @@ The engine has no opinion about local branches; this hygiene is the operator's r
|
|
|
119
121
|
|
|
120
122
|
Live-checkout mode is deliberately small. These are NOT supported and will not be added:
|
|
121
123
|
|
|
122
|
-
- **No `auto` mode.** The choice between `
|
|
124
|
+
- **No `auto` mode.** The choice between `worktree` and `live` is per-project and operator-set. The engine will not auto-detect submodules / `repo` workspaces and silently switch modes.
|
|
123
125
|
- **No auto-stash on dirty refusal.** The engine refuses and exits; it never `git stash`es to "make room" for a dispatch. Stashes silently mutate the operator's tree and conflate engine state with operator state.
|
|
124
126
|
- **No concurrent dispatches per project.** The cap is 1; raising it would require per-WI subdirectories, which live mode explicitly does not provide.
|
|
125
|
-
- **No per-WI subdirectory isolation.** Live mode is one-checkout-per-project by design. If you need isolation, use `
|
|
126
|
-
- **No per-WI override.** `
|
|
127
|
+
- **No per-WI subdirectory isolation.** Live mode is one-checkout-per-project by design. If you need isolation, use `checkoutMode: 'worktree'` (the default).
|
|
128
|
+
- **No per-WI override.** `checkoutMode` is per-project only. There is no `meta.checkoutMode` on a work item that overrides the project setting.
|
|
127
129
|
- **No auto-pull / no fast-forward on existing branches.** See Guarantee 3. If a PR branch is checked out locally at a different SHA than `origin/<branch>`, the operator resolves it manually.
|
|
128
130
|
- **No special timeout / kill handling.** Live-mode dispatches are killed by PID exactly like isolated-mode dispatches (`engine/timeout.js` header comment). The engine sends SIGTERM/SIGKILL to the tracked process and never touches the working tree on kill.
|
|
129
131
|
|
|
@@ -131,13 +133,13 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
131
133
|
|
|
132
134
|
| File | Purpose |
|
|
133
135
|
|---|---|
|
|
134
|
-
| `engine/shared.js` — `
|
|
136
|
+
| `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
|
|
135
137
|
| `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
|
|
136
138
|
| `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper: dirty check, branch resolution from HEAD (no fetch, no `origin/<mainRef>` — issue #226) (P-a3f9b203). |
|
|
137
139
|
| `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
|
|
138
140
|
| `engine.js` — dispatcher `liveProjectsInUse` set | Per-project mutating-concurrency cap (P-a3f9b205). |
|
|
139
141
|
| `engine.js` — worktree-pool / orphan-GC short-circuits | `worktreePath===null` no-ops in live mode (P-a3f9b206). |
|
|
140
|
-
| `dashboard/js/settings.js` —
|
|
142
|
+
| `dashboard/js/settings.js` — checkoutMode dropdown + chip | Operator-facing UI (P-a3f9b207). |
|
|
141
143
|
| `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring}.test.js` | Wiring and contract tests (P-a3f9b208). |
|
|
142
144
|
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` | Non-retryable refusal class. |
|
|
143
145
|
| `engine/dispatch.js` — `isRetryableFailureReason` neverRetry | Excludes `LIVE_CHECKOUT_DIRTY` from mechanical retry. |
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Spec: Agent & Role Configurability
|
|
2
|
+
|
|
3
|
+
**Status:** Draft
|
|
4
|
+
**Owner:** @calebt
|
|
5
|
+
**Audience:** Minions contributors working on the dashboard, engine config surface, and playbook/system-prompt rendering.
|
|
6
|
+
|
|
7
|
+
> Companion reading: [`.github/copilot-instructions.md`](../../.github/copilot-instructions.md) and [`CLAUDE.md`](../../CLAUDE.md) for the engine/dashboard split and config conventions.
|
|
8
|
+
|
|
9
|
+
## 1. Motivation
|
|
10
|
+
|
|
11
|
+
Today a Minions "team" is a fixed roster of five hard-coded agents (`ripley`, `dallas`, `lambert`, `rebecca`, `ralph`) whose personas live in a mix of places: a free-text `role` string in `config.json`, a loosely-related `skills` string array, and a per-agent `charter.md` file. There is **no way to**:
|
|
12
|
+
|
|
13
|
+
- See an agent's full definition (persona + skills + runtime) in one place.
|
|
14
|
+
- Treat a "role" as a reusable, editable unit — it is just a label string today.
|
|
15
|
+
- Add a brand-new role, or stand up a new named agent, from the dashboard.
|
|
16
|
+
|
|
17
|
+
The goal is to make the fleet **user-configurable** through a **Role / Agent Library**, delivered in two phases:
|
|
18
|
+
|
|
19
|
+
- **Phase 1 — Expose & edit.** Surface the skill + definition for each existing role/agent in one editable library view. No new roles or agents yet; just make what exists first-class and editable.
|
|
20
|
+
- **Phase 2 — Create & assign.** Add new roles, and stand up new named agents bound to a role.
|
|
21
|
+
|
|
22
|
+
## 2. Current state (grounding)
|
|
23
|
+
|
|
24
|
+
This section is the factual baseline the design must respect. Citations are `file:line` against the repo at spec-authoring time.
|
|
25
|
+
|
|
26
|
+
### 2.1 Agent object
|
|
27
|
+
|
|
28
|
+
Agents live under `config.agents` keyed by lowercase id. Defaults are hard-coded:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// engine/shared.js:4350-4356
|
|
32
|
+
const DEFAULT_AGENTS = {
|
|
33
|
+
ripley: { name: 'Ripley', emoji: '🏗️', role: 'Lead / Explorer', skills: ['architecture', 'codebase-exploration', 'design-review'] },
|
|
34
|
+
dallas: { name: 'Dallas', emoji: '🔧', role: 'Engineer', skills: ['implementation', 'typescript', 'docker', 'testing'] },
|
|
35
|
+
lambert: { name: 'Lambert', emoji: '📊', role: 'Analyst', skills: ['gap-analysis', 'requirements', 'documentation'] },
|
|
36
|
+
rebecca: { name: 'Rebecca', emoji: '🧠', role: 'Architect', skills: ['system-design', 'api-design', 'scalability', 'implementation'] },
|
|
37
|
+
ralph: { name: 'Ralph', emoji: '⚙️', role: 'Engineer', skills: ['implementation', 'bug-fixes', 'testing', 'scaffolding'] },
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Runtime / budget fields are **optional per-agent overrides** resolved against `engine.*` fleet defaults via helpers in `engine/shared.js`:
|
|
42
|
+
|
|
43
|
+
| Field | Resolver | Falls back to |
|
|
44
|
+
|-------|----------|---------------|
|
|
45
|
+
| `cli` | `resolveAgentCli` (shared.js:3142) | `engine.defaultCli` → `'copilot'` |
|
|
46
|
+
| `model` | `resolveAgentModel` (shared.js:3202) | `engine.defaultModel` → undefined |
|
|
47
|
+
| `maxBudgetUsd` / `monthlyBudgetUsd` | `resolveAgentMaxBudget` (shared.js:3231) | `engine.maxBudgetUsd` |
|
|
48
|
+
| `bareMode` | `resolveAgentBareMode` (shared.js:3254) | `engine.claudeBareMode` → false |
|
|
49
|
+
|
|
50
|
+
So the effective agent shape is:
|
|
51
|
+
|
|
52
|
+
```jsonc
|
|
53
|
+
{ "name": "Dallas", "emoji": "🔧", "role": "Engineer",
|
|
54
|
+
"skills": ["implementation","testing"],
|
|
55
|
+
"cli": "claude", "model": "haiku", "monthlyBudgetUsd": 5, "bareMode": false }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2.2 "Role" is a label, not an entity
|
|
59
|
+
|
|
60
|
+
`role` is a **free-text string** on the agent. There is no role registry, no role schema, and routing does **not** reference roles — it references agent ids. The dashboard edits role as a plain text input (`dashboard/js/settings.js:72`) and persists it verbatim (`dashboard.js:10684`).
|
|
61
|
+
|
|
62
|
+
### 2.3 `skills` is metadata, not real skills
|
|
63
|
+
|
|
64
|
+
Two unrelated things share the word "skill":
|
|
65
|
+
|
|
66
|
+
1. **`agent.skills`** — a string array, surfaced only as an `Expertise: …` line in the system prompt (`engine/playbook.js:950`). It is **not** linked to any executable capability.
|
|
67
|
+
2. **Real `SKILL.md` files** — discovered from disk (`.claude/skills/`, `~/.claude/skills`, project harness dirs) by `collectSkillFiles()` / `getSkills()` (`engine/queries.js:1263-1400`), scoped global/project/plugin. These are **not** bound to a specific agent or role; every agent sees the same discovered catalog.
|
|
68
|
+
|
|
69
|
+
This split is a key source of the "agents aren't really configurable" feeling and must be addressed explicitly (see §4.3).
|
|
70
|
+
|
|
71
|
+
### 2.4 Charter = the real "agent definition"
|
|
72
|
+
|
|
73
|
+
The substantive persona/instructions for an agent is `agents/<id>/charter.md`, read by `getAgentCharter()` (`engine/queries.js:677-678`) and injected into the system prompt as `## Your Charter` (`engine/playbook.js:942-954`). The system prompt assembled in `buildSystemPrompt()` is:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
# You are {name} ({role})
|
|
77
|
+
Agent ID: {id}
|
|
78
|
+
Expertise: {skills joined}
|
|
79
|
+
|
|
80
|
+
## Your Charter
|
|
81
|
+
{charter.md contents}
|
|
82
|
+
```
|
|
83
|
+
(`engine/playbook.js:940-955`)
|
|
84
|
+
|
|
85
|
+
Charter is already editable today, but only from the **agent detail panel → Charter tab** (`dashboard/js/detail-panel.js:115-126`) via `POST /api/agents/charter` (`dashboard.js:13205-13211`). It is disconnected from the Settings → Agents table.
|
|
86
|
+
|
|
87
|
+
### 2.5 Routing
|
|
88
|
+
|
|
89
|
+
`routing.md` is a markdown table mapping work-type → preferred/fallback **agent id** (`routing.md:8-27`), parsed by `engine/routing.js:31-52`. `resolveAgent()` (`engine/routing.js:192`) resolves the route, honoring `_author_` / `_any_` tokens, idleness, budget, and the self-review ban. Routing is edited via `POST /api/settings/routing`, which overwrites `routing.md` wholesale (`dashboard.js:10799-10805`).
|
|
90
|
+
|
|
91
|
+
### 2.6 Config read / write surface
|
|
92
|
+
|
|
93
|
+
- **Read:** `queries.getConfig()` (`engine/queries.js:181-225`); Settings read endpoint returns `{ engine, claude, agents, projects, routing }` (`dashboard.js:10362-10391`).
|
|
94
|
+
- **Write:** `POST /api/settings` edits `engine`, `claude`, **existing** `agents[id]` (role, skills, cli, model, budget — `dashboard.js:10682-10705`), and projects, then saves `config.json`. **There is no agent create/delete path** — the loop `for (const [id, updates] of Object.entries(body.agents))` skips unknown ids (`dashboard.js:10683`).
|
|
95
|
+
- **Reset:** restores `engine` defaults + `shared.DEFAULT_AGENTS` (`dashboard.js:10808-10815`).
|
|
96
|
+
- **UI:** Settings → Runtime pane has an **edit-only** Agents table (Role, Skills, CLI, Model, Budget) — `dashboard/js/settings.js:152-157`. No add/remove.
|
|
97
|
+
|
|
98
|
+
### 2.7 Gaps summary
|
|
99
|
+
|
|
100
|
+
| Capability | Today |
|
|
101
|
+
|------------|-------|
|
|
102
|
+
| View an agent's full definition in one place | ✗ (split across settings table + charter tab) |
|
|
103
|
+
| Role as a reusable, editable unit | ✗ (free-text label) |
|
|
104
|
+
| Edit charter from the library/settings | ✗ (only detail panel) |
|
|
105
|
+
| Bind skills to a role/agent meaningfully | ✗ (metadata only) |
|
|
106
|
+
| Add a new role | ✗ |
|
|
107
|
+
| Add a new named agent | ✗ |
|
|
108
|
+
| Delete an agent | ✗ |
|
|
109
|
+
|
|
110
|
+
## 3. Target model: the Role / Agent Library
|
|
111
|
+
|
|
112
|
+
Introduce a **Role** as a first-class, reusable definition, and make **Agents** named instances that reference a role and optionally override pieces of it.
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
// config.json (additive — see §5 migration)
|
|
116
|
+
{
|
|
117
|
+
"roles": {
|
|
118
|
+
"engineer": {
|
|
119
|
+
"label": "Engineer", // human-facing name
|
|
120
|
+
"definition": "<markdown persona>", // the role-level charter (system-prompt body)
|
|
121
|
+
"skills": ["implementation", "testing", "bug-fixes"],
|
|
122
|
+
"defaultCli": null, // optional role-level runtime defaults
|
|
123
|
+
"defaultModel": null,
|
|
124
|
+
"builtin": true // seeded from DEFAULT_ROLES; protects from delete in P1
|
|
125
|
+
},
|
|
126
|
+
"architect": { "label": "Architect", "definition": "…", "skills": ["system-design","api-design"] }
|
|
127
|
+
},
|
|
128
|
+
"agents": {
|
|
129
|
+
"dallas": {
|
|
130
|
+
"name": "Dallas", "emoji": "🔧",
|
|
131
|
+
"role": "engineer", // now references roles{} key (was free text)
|
|
132
|
+
"skills": [], // agent-level ADDITIONS to the role's skills
|
|
133
|
+
"definitionOverride": null, // optional per-agent charter override
|
|
134
|
+
"cli": null, "model": null, "monthlyBudgetUsd": null, "bareMode": false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Resolution rules (engine):**
|
|
141
|
+
|
|
142
|
+
- Effective **definition** = `agent.definitionOverride` ?? `roles[agent.role].definition` ?? legacy `agents/<id>/charter.md`. (Keeps existing charters working — see §5.)
|
|
143
|
+
- Effective **skills** = union of `roles[agent.role].skills` and `agent.skills`.
|
|
144
|
+
- Effective **label** shown in `# You are {name} ({label})` = `roles[agent.role].label` ?? raw `agent.role` string (legacy).
|
|
145
|
+
- Effective **cli/model** chain unchanged, with an inserted role tier: `agent.cli` → `roles[agent.role].defaultCli` → `engine.defaultCli` → `'copilot'`.
|
|
146
|
+
|
|
147
|
+
This preserves every existing resolver's external behavior when `roles` is absent or when `agent.role` is still a free-text string (legacy), and layers the library in additively.
|
|
148
|
+
|
|
149
|
+
## 4. Phase 1 — Expose & edit (no new roles/agents)
|
|
150
|
+
|
|
151
|
+
**User story:** "For each of the different roles, I want to see and edit the skill and agent definition."
|
|
152
|
+
|
|
153
|
+
### 4.1 Scope
|
|
154
|
+
|
|
155
|
+
- A **Role / Agent Library** view in the dashboard listing the existing roles and the agents bound to each.
|
|
156
|
+
- For each role: edit its **definition** (markdown persona) and **skills** list.
|
|
157
|
+
- For each agent: edit its definition override + skills + runtime (consolidating the Settings table fields and the Charter tab into one place).
|
|
158
|
+
- **No** create/delete of roles or agents in Phase 1.
|
|
159
|
+
|
|
160
|
+
### 4.2 Data model (Phase 1)
|
|
161
|
+
|
|
162
|
+
- Add `DEFAULT_ROLES` to `engine/shared.js`, derived from the distinct `role` strings in `DEFAULT_AGENTS` (`Lead / Explorer`, `Engineer`, `Analyst`, `Architect`). Seed each role's `definition` from the corresponding agent's existing `charter.md` content where a 1:1 mapping exists; otherwise leave a stub.
|
|
163
|
+
- Migrate `agent.role` free-text → role key on first load (in-memory, mirroring `applyLegacyCcModelMigration`'s no-disk-rewrite pattern in shared.js) by slugifying the label and matching against `roles{}`. Unmatched strings remain as legacy free text and are rendered as an ad-hoc role.
|
|
164
|
+
- Introduce the resolution rules in §3 behind the existing resolver helpers so engine behavior is unchanged when `roles` is absent.
|
|
165
|
+
|
|
166
|
+
### 4.3 Skills: decide the binding (REQUIRED for this phase)
|
|
167
|
+
|
|
168
|
+
Phase 1 must resolve the "two meanings of skill" ambiguity. Proposed: keep `roles[].skills` / `agent.skills` as **skill references** (tags), and in the system prompt render them as the `Expertise:` line **plus** — when a tag matches a discovered `SKILL.md` name from `getSkills()` — surface that skill's description inline. This makes the skills list meaningful without forcing a disk-layout change in P1. (Full per-role skill *scoping* of `collectSkillFiles` is deferred to a later phase; see §7.)
|
|
169
|
+
|
|
170
|
+
### 4.4 API (Phase 1)
|
|
171
|
+
|
|
172
|
+
- `GET /api/roles` → `{ roles: { <key>: { label, definition, skills, defaultCli, defaultModel, builtin, agentIds[] } } }`. Aggregates `config.roles` + reverse-maps `config.agents` by role.
|
|
173
|
+
- `POST /api/roles/update` → `{ key, definition?, skills?, defaultCli?, defaultModel? }`. Edits an existing role only (P1). Writes through `mutateJsonFileLocked` on `config.json` (never `safeWrite` — see CLAUDE.md concurrency rules).
|
|
174
|
+
- Extend `POST /api/settings` agent loop to accept `definitionOverride`.
|
|
175
|
+
- Reuse/retire `POST /api/agents/charter`: in P1 it continues to write `agents/<id>/charter.md` for backward-compat, but the library writes `definitionOverride` into config; the resolver prefers the override. (Deprecation tracked in `docs/deprecated.json`.)
|
|
176
|
+
|
|
177
|
+
### 4.5 UI (Phase 1)
|
|
178
|
+
|
|
179
|
+
- New **Library** entry on the Settings rail (or a top-level sidebar page — decide in review). Layout: left column = role list; right pane = selected role's definition editor (markdown textarea with preview, reusing the Charter tab's editor pattern from `detail-panel.js`) + skills chips + the agents bound to that role (each expandable to its per-agent overrides).
|
|
180
|
+
- Definition editor reuses the existing markdown view/edit toggle and `renderMd()` escaping already used by the Charter tab (`dashboard/js/detail-panel.js:115-126`) to satisfy the no-unsanitized lint gate (`npm run lint`).
|
|
181
|
+
- Settings → Agents table gains a link/affordance into the Library for the definition (so the two surfaces don't drift).
|
|
182
|
+
|
|
183
|
+
### 4.6 Phase 1 acceptance criteria
|
|
184
|
+
|
|
185
|
+
1. Library view lists every existing role with its definition + skills, and the agents under each.
|
|
186
|
+
2. Editing a role's definition changes the system prompt for every agent bound to that role (verified by `buildSystemPrompt` output).
|
|
187
|
+
3. Editing an agent's definition override wins over the role definition for that agent only.
|
|
188
|
+
4. Engine behavior is **unchanged** when `config.roles` is absent (back-compat).
|
|
189
|
+
5. All writes go through `mutateJsonFileLocked`; no `safeWrite` on `config.json`.
|
|
190
|
+
6. `npm test` green (add source-inspection + resolver unit tests near existing agent/charter tests); `npm run lint` green.
|
|
191
|
+
7. Settings reset still restores a working fleet (now `DEFAULT_ROLES` + `DEFAULT_AGENTS`).
|
|
192
|
+
|
|
193
|
+
## 5. Migration & backward compatibility
|
|
194
|
+
|
|
195
|
+
- **Additive config.** `roles` is new and optional. Absent `roles` ⇒ legacy path (free-text role label, per-agent `charter.md`). No forced disk rewrite — migration is in-memory at load, matching `applyLegacyCcModelMigration`.
|
|
196
|
+
- **Charters preserved.** `agents/<id>/charter.md` remains readable and is the lowest-priority definition source. `minions init`/upgrade already preserves charter files (`bin/minions.js:1361-1368`) — do not regress that.
|
|
197
|
+
- **Routing unaffected in P1/P2-by-agent.** Routing still targets agent ids. (Routing-by-role is explicitly out of scope; see §7.)
|
|
198
|
+
- **Deprecation.** If `POST /api/agents/charter` is superseded by the library's `definitionOverride`, add a `docs/deprecated.json` entry with a cleanup path.
|
|
199
|
+
|
|
200
|
+
## 6. Phase 2 — Create & assign
|
|
201
|
+
|
|
202
|
+
**User story:** "Add new roles, and assign new named agents with these roles."
|
|
203
|
+
|
|
204
|
+
### 6.1 Scope
|
|
205
|
+
|
|
206
|
+
- **Role CRUD:** create/rename/delete roles in the library. Deleting a role is blocked while agents reference it (or offers reassignment). `builtin` roles are delete-protected.
|
|
207
|
+
- **Agent CRUD:** create a new named agent (id + name + emoji + role + optional overrides) and delete a non-builtin agent.
|
|
208
|
+
- New agents become immediately routable (they appear to `resolveAgent` via `config.agents`) and selectable in `routing.md` (manual) — auto-wiring into routing is out of scope.
|
|
209
|
+
|
|
210
|
+
### 6.2 Data model / engine
|
|
211
|
+
|
|
212
|
+
- New agent ids validated: lowercase slug, unique, not colliding with `temp-*` reserved prefix.
|
|
213
|
+
- `resolveAgent` already iterates `config.agents` generically, so new agents are picked up with no engine change (`engine/routing.js:192-249`). Deleting an agent must also: cancel/skip pending dispatches targeting it, and leave its history intact (mirror the conservative posture of project removal in `engine/projects.js`).
|
|
214
|
+
- A new agent with no `charter.md` and no `definitionOverride` inherits its role's definition — this is the payoff of §3.
|
|
215
|
+
|
|
216
|
+
### 6.3 API (Phase 2)
|
|
217
|
+
|
|
218
|
+
- `POST /api/roles/create` `{ key, label, definition, skills, defaultCli?, defaultModel? }`.
|
|
219
|
+
- `POST /api/roles/delete` `{ key, reassignTo? }` — refuses if agents reference it and no `reassignTo` given; refuses on `builtin`.
|
|
220
|
+
- `POST /api/agents/create` `{ id, name, emoji, role, overrides? }`.
|
|
221
|
+
- `POST /api/agents/delete` `{ id }` — refuses on builtin; drains/cancels pending dispatches first.
|
|
222
|
+
- Extend `POST /api/settings/reset` semantics: reset clears user-created roles/agents back to `DEFAULT_ROLES`/`DEFAULT_AGENTS`.
|
|
223
|
+
|
|
224
|
+
### 6.4 UI (Phase 2)
|
|
225
|
+
|
|
226
|
+
- "+ New role" and "+ New agent" affordances in the Library.
|
|
227
|
+
- Agent creation form (id/name/emoji/role picker + optional runtime overrides).
|
|
228
|
+
- Delete with confirmation + reassignment flow for roles in use.
|
|
229
|
+
|
|
230
|
+
### 6.5 Phase 2 acceptance criteria
|
|
231
|
+
|
|
232
|
+
1. A user can create a role with a definition + skills and see it in the library.
|
|
233
|
+
2. A user can create a named agent bound to a role; with no override it inherits the role definition; the engine can route work to it.
|
|
234
|
+
3. Deleting a role in use is blocked or forces reassignment; builtin roles/agents cannot be deleted.
|
|
235
|
+
4. Deleting an agent cancels its pending dispatches and preserves completed history.
|
|
236
|
+
5. Settings reset returns to the default fleet.
|
|
237
|
+
6. `npm test` + `npm run lint` green; new CRUD covered by unit tests; concurrency via `mutateJsonFileLocked`.
|
|
238
|
+
|
|
239
|
+
## 7. Out of scope (explicitly deferred)
|
|
240
|
+
|
|
241
|
+
- **Routing-by-role** (e.g. `routing.md` rows targeting a role instead of an agent id). Routing stays agent-id-based.
|
|
242
|
+
- **Per-role/per-agent skill *scoping*** of on-disk `SKILL.md` discovery (`collectSkillFiles` stays global/project/plugin). P1 only makes the skills *list* meaningful in the prompt (§4.3).
|
|
243
|
+
- **Per-project role overrides** (`projects/<name>/...`).
|
|
244
|
+
- **Importing/sharing role libraries** across installs.
|
|
245
|
+
- Any change to the meeting/plan agent-charter linkage beyond reading the new definition source.
|
|
246
|
+
|
|
247
|
+
## 8. Open questions (resolve in review)
|
|
248
|
+
|
|
249
|
+
1. **Library placement** — Settings rail entry vs. a dedicated top-level sidebar page?
|
|
250
|
+
2. **Role↔agent cardinality framing** — do we surface roles as the primary list (agents nested under them), or keep agents primary with role as an attribute? (Spec assumes roles-primary in the Library; agents-primary stays in Settings.)
|
|
251
|
+
3. **Charter vs definition naming** — adopt "definition" everywhere, or keep "charter" as the user-facing term to avoid churn in existing UI/tests?
|
|
252
|
+
4. **Skills binding (§4.3)** — ship the "tag matches discovered SKILL.md → inline description" behavior in P1, or keep skills purely as the `Expertise:` line until a later phase?
|
|
253
|
+
5. **Default role definitions** — seed from existing per-agent `charter.md`, or author fresh role-level definitions?
|
|
254
|
+
|
|
255
|
+
## 9. Work breakdown (suggested PRD seeds)
|
|
256
|
+
|
|
257
|
+
**Phase 1**
|
|
258
|
+
- P1-a: `DEFAULT_ROLES` + in-memory legacy role migration + resolver tiering (engine/shared.js, playbook.js).
|
|
259
|
+
- P1-b: `GET /api/roles`, `POST /api/roles/update`, `definitionOverride` on `POST /api/settings` (dashboard.js).
|
|
260
|
+
- P1-c: Library UI (role list + definition editor + skills + nested agents) reusing the charter editor pattern.
|
|
261
|
+
- P1-d: Tests (resolver unit tests, source-inspection for endpoints/UI) + `docs/deprecated.json` entry if `/api/agents/charter` is superseded + settings parity note in CLAUDE.md.
|
|
262
|
+
|
|
263
|
+
**Phase 2**
|
|
264
|
+
- P2-a: Role CRUD API + delete-guard/reassignment.
|
|
265
|
+
- P2-b: Agent CRUD API + dispatch-drain on delete + id validation.
|
|
266
|
+
- P2-c: Library create/delete UI flows.
|
|
267
|
+
- P2-d: Tests + reset semantics + docs.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
_Every new `engine.*`/config surface introduced here must also get a dashboard control and be documented in CLAUDE.md — config without UI is incomplete (see CLAUDE.md "Best Practices" #15)._
|
package/engine/cleanup.js
CHANGED
|
@@ -567,7 +567,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
567
567
|
// change to both locations, producing mirror-write leaks.
|
|
568
568
|
// We only WARN here — removing someone else's worktree without consent could
|
|
569
569
|
// destroy in-flight work. The operator runs `git worktree remove <path>`.
|
|
570
|
-
// P-a3f9b206: live-mode projects (
|
|
570
|
+
// P-a3f9b206: live-mode projects (checkoutMode === 'live') intentionally
|
|
571
571
|
// never create linked worktrees — `git worktree list` returns only the
|
|
572
572
|
// operator's main checkout, which `shared.isPathInside` correctly excludes
|
|
573
573
|
// (equal-path is not "inside"). An empty/main-only nested-worktree scan
|
package/engine/pr-clone-keep.js
CHANGED
|
@@ -264,7 +264,7 @@ function _record(plan, fields) {
|
|
|
264
264
|
* - `config` — full config; re-checks configured-project so a stale plan can't
|
|
265
265
|
* re-clone a repo that is already linked.
|
|
266
266
|
* - `registerOptions` — passed through to projects.addProject (name override,
|
|
267
|
-
*
|
|
267
|
+
* checkoutMode, observeAuthors, …).
|
|
268
268
|
* - test seams: `gitClone`, `addProject`, `findConfiguredProject`, `fetchPrBranches`,
|
|
269
269
|
* `token`/`_resolveTokenForSlug`/`_acquireAdoToken`, `cloneRoot`, `_rm`, `_mkdir`.
|
|
270
270
|
*
|
package/engine/preflight.js
CHANGED
|
@@ -12,6 +12,7 @@ const fs = require('fs');
|
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { execSync, execFileSync } = require('child_process');
|
|
15
|
+
const shared = require('./shared');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Resolve the Claude Code CLI binary path. Legacy helper preserved for back-
|
|
@@ -190,13 +191,20 @@ function runPreflight(opts = {}) {
|
|
|
190
191
|
const results = [];
|
|
191
192
|
let allOk = true;
|
|
192
193
|
|
|
193
|
-
// 1. Node.js version >=
|
|
194
|
+
// 1. Node.js version >= 22.5.0 (built-in node:sqlite — issue #244). HARD FAIL
|
|
195
|
+
// on older Node: node:sqlite is the only state backend post Phase 9.4, and
|
|
196
|
+
// the --experimental-sqlite self-reexec is a no-op below 22.5, so every
|
|
197
|
+
// getDb() throws a raw stack trace. Numeric comparison (shared helper) so
|
|
198
|
+
// the 22.4.x → 22.5.0 boundary is correct and we never string-compare.
|
|
194
199
|
const nodeVersion = process.versions.node;
|
|
195
|
-
|
|
196
|
-
if (major >= 18) {
|
|
200
|
+
if (shared.nodeSupportsBuiltinSqlite(nodeVersion)) {
|
|
197
201
|
results.push({ name: 'Node.js', ok: true, message: `v${nodeVersion}` });
|
|
198
202
|
} else {
|
|
199
|
-
|
|
203
|
+
// Message mirrors the canonical CLI-preflight remediation (minus the
|
|
204
|
+
// leading "Node.js: " label, which printPreflight already renders as the
|
|
205
|
+
// check name) so doctor and the preflight read identically.
|
|
206
|
+
const remediation = shared.nodeSqliteRemediationLine(nodeVersion).replace(/^Node\.js:\s*/, '');
|
|
207
|
+
results.push({ name: 'Node.js', ok: false, message: remediation });
|
|
200
208
|
allOk = false;
|
|
201
209
|
}
|
|
202
210
|
|
|
@@ -239,12 +247,9 @@ function runPreflight(opts = {}) {
|
|
|
239
247
|
// us the config. checkOrExit() / cli start() / doctor() pass it; legacy
|
|
240
248
|
// callers don't, in which case we skip silently.
|
|
241
249
|
if (opts && opts.config && typeof opts.config === 'object') {
|
|
242
|
-
//
|
|
243
|
-
// workSources warnings and the
|
|
244
|
-
//
|
|
245
|
-
// undefined identifier (ReferenceError silently caught by the wrapping
|
|
246
|
-
// try/catch).
|
|
247
|
-
const shared = require('./shared');
|
|
250
|
+
// `shared` is module-level (required at top of file) and referenced by
|
|
251
|
+
// every check block below, including workSources warnings and the
|
|
252
|
+
// worktreeRoot check.
|
|
248
253
|
try {
|
|
249
254
|
let runtimeNames = [];
|
|
250
255
|
try { runtimeNames = require('./runtimes').listRuntimes(); }
|
|
@@ -544,6 +549,13 @@ function doctor(minionsHome) {
|
|
|
544
549
|
catch { /* missing/invalid config is its own check below */ }
|
|
545
550
|
const { passed, results } = runPreflight({ config: preflightConfig });
|
|
546
551
|
|
|
552
|
+
// When Node is too old for built-in node:sqlite (issue #244) the engine
|
|
553
|
+
// cannot boot, so the stale-engine PID check and per-runtime model-discovery
|
|
554
|
+
// probes are GUARANTEED to fail/warn — noise that buries the one root cause.
|
|
555
|
+
// De-prioritize them: skip the model-discovery network probe entirely and
|
|
556
|
+
// mute the stale-PID entry to a single "expected — fix Node first" note.
|
|
557
|
+
const nodeCriticalFail = results.some(r => r.name === 'Node.js' && r.ok === false);
|
|
558
|
+
|
|
547
559
|
// Runtime checks
|
|
548
560
|
const runtimeResults = [];
|
|
549
561
|
|
|
@@ -600,7 +612,13 @@ function doctor(minionsHome) {
|
|
|
600
612
|
alive = true;
|
|
601
613
|
}
|
|
602
614
|
} catch { /* process may be dead */ }
|
|
603
|
-
|
|
615
|
+
if (!alive && nodeCriticalFail) {
|
|
616
|
+
// Expected fallout of the Node version failure above — don't add a
|
|
617
|
+
// second critical fail competing for the operator's attention.
|
|
618
|
+
runtimeResults.push({ name: 'Engine', ok: 'warn', message: `stale PID ${control.pid} — expected while Node is too old; fix the Node.js check above first` });
|
|
619
|
+
} else {
|
|
620
|
+
runtimeResults.push({ name: 'Engine', ok: alive, message: alive ? `running (PID ${control.pid})` : `stale PID ${control.pid} — run: minions start (see docs/engine-restart.md)` });
|
|
621
|
+
}
|
|
604
622
|
} else {
|
|
605
623
|
runtimeResults.push({ name: 'Engine', ok: 'warn', message: `${control.state || 'stopped'} — run: minions start (see docs/engine-restart.md)` });
|
|
606
624
|
}
|
|
@@ -641,8 +659,15 @@ function doctor(minionsHome) {
|
|
|
641
659
|
// avoids a second JSON.parse round-trip.
|
|
642
660
|
const fleetSummary = _fleetSummaryResults(preflightConfig);
|
|
643
661
|
runtimeResults.push(...fleetSummary);
|
|
644
|
-
|
|
645
|
-
|
|
662
|
+
if (nodeCriticalFail) {
|
|
663
|
+
// Model discovery shells out to the runtime CLI / REST API, which can't
|
|
664
|
+
// work while the engine can't boot. Skip the probe and emit one muted
|
|
665
|
+
// note instead of a per-runtime "discovery unavailable" warning storm.
|
|
666
|
+
runtimeResults.push({ name: 'Models', ok: 'warn', message: 'discovery skipped — fix the Node.js check above first (engine cannot boot on this Node)' });
|
|
667
|
+
} else {
|
|
668
|
+
const modelResults = await _modelDiscoveryResults(preflightConfig);
|
|
669
|
+
runtimeResults.push(...modelResults);
|
|
670
|
+
}
|
|
646
671
|
// Verify each runtime CLI still recognizes the headless bypass flags the
|
|
647
672
|
// adapters inject. Catches "user installed an outdated CLI" before the
|
|
648
673
|
// first agent silently hangs on a permission prompt.
|
|
@@ -432,15 +432,16 @@ function buildPrUrlBase({ repoHost, org, project, repoName, prUrlBase }) {
|
|
|
432
432
|
return '';
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
function buildProjectEntry({ name, description, localPath, repoHost, repositoryId, org, project, repoName, mainBranch, prUrlBase,
|
|
435
|
+
function buildProjectEntry({ name, description, localPath, repoHost, repositoryId, org, project, repoName, mainBranch, prUrlBase, checkoutMode, observeAuthors }) {
|
|
436
436
|
const safeName = (name || 'project').replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'project';
|
|
437
437
|
const host = repoHost || 'github';
|
|
438
438
|
const isAdo = host === 'ado';
|
|
439
|
-
// P-a3f9b201:
|
|
440
|
-
// field is omitted and resolves to '
|
|
441
|
-
// 400 Error on unknown values. Run validation
|
|
442
|
-
// typo at the dashboard or CLI never lands in
|
|
443
|
-
|
|
439
|
+
// P-a3f9b201 (consolidated W-mqiaw974): validateCheckoutMode returns undefined
|
|
440
|
+
// on absent/empty (so the field is omitted and resolves to 'worktree'
|
|
441
|
+
// downstream) and throws an HTTP 400 Error on unknown values. Run validation
|
|
442
|
+
// BEFORE building the entry so a typo at the dashboard or CLI never lands in
|
|
443
|
+
// config.json.
|
|
444
|
+
const resolvedCheckoutMode = shared.validateCheckoutMode(checkoutMode);
|
|
444
445
|
const entry = {
|
|
445
446
|
name: safeName,
|
|
446
447
|
description: description || '',
|
|
@@ -458,7 +459,7 @@ function buildProjectEntry({ name, description, localPath, repoHost, repositoryI
|
|
|
458
459
|
workItems: { enabled: true, cooldownMinutes: 0 },
|
|
459
460
|
},
|
|
460
461
|
};
|
|
461
|
-
if (
|
|
462
|
+
if (resolvedCheckoutMode !== undefined) entry.checkoutMode = resolvedCheckoutMode;
|
|
462
463
|
// W-mq8li79a000889fa — observeAuthors is the canonical list of author
|
|
463
464
|
// identifiers consumed by the `ado-author-prs` watch (and any future
|
|
464
465
|
// author-scoped watch). Currently only meaningful for ADO projects —
|
package/engine/projects.js
CHANGED
|
@@ -535,7 +535,7 @@ async function addProject(target, options = {}) {
|
|
|
535
535
|
repoName: detected.repoName || name,
|
|
536
536
|
mainBranch: detected.mainBranch || 'main',
|
|
537
537
|
prUrlBase: detected.prUrlBase,
|
|
538
|
-
|
|
538
|
+
checkoutMode: options.checkoutMode ?? options.worktreeMode,
|
|
539
539
|
observeAuthors: Array.isArray(options.observeAuthors) ? options.observeAuthors : undefined,
|
|
540
540
|
});
|
|
541
541
|
|
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.2216",
|
|
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"
|