@yemi33/minions 0.1.1927 → 0.1.1929

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.
@@ -98,6 +98,19 @@ function _hashMcpServers(mcpServers) {
98
98
  return crypto.createHash('sha256').update(json).digest('hex');
99
99
  }
100
100
 
101
+ // Bug B (issue #2479): `model` and `effort` were stored on the Worker but
102
+ // never forwarded to ACP `session/new`, so any `ccModel` override was silently
103
+ // dropped on the pool path. Build the params object here and only include
104
+ // fields that are defined — sending `model: undefined` would force the daemon
105
+ // onto its built-in default instead of letting it pick whatever the user has
106
+ // configured globally.
107
+ function _buildSessionNewParams({ cwd, mcpServers, model, effort }) {
108
+ const params = { cwd, mcpServers: mcpServers || [] };
109
+ if (model !== undefined && model !== null && model !== '') params.model = model;
110
+ if (effort !== undefined && effort !== null && effort !== '') params.effort = effort;
111
+ return params;
112
+ }
113
+
101
114
  class Worker {
102
115
  constructor({ tabId, model, effort, mcpServers, mcpServersHash, systemPromptHash, cwd }) {
103
116
  this.tabId = tabId;
@@ -170,7 +183,9 @@ class Worker {
170
183
  earlyExitPromise,
171
184
  ]);
172
185
  const result = await Promise.race([
173
- this._call('session/new', { cwd: this.cwd, mcpServers: this.mcpServers }),
186
+ this._call('session/new', _buildSessionNewParams({
187
+ cwd: this.cwd, mcpServers: this.mcpServers, model: this.model, effort: this.effort,
188
+ })),
174
189
  earlyExitPromise,
175
190
  ]);
176
191
  this.sessionId = result && result.sessionId;
@@ -344,7 +359,7 @@ class Worker {
344
359
  this._notify('session/cancel', { sessionId: this.inflight.sessionId });
345
360
  }
346
361
 
347
- async newSession({ mcpServers, systemPromptHash }) {
362
+ async newSession({ mcpServers, systemPromptHash, model, effort }) {
348
363
  // Cancel any inflight before swapping the underlying session.
349
364
  if (this.inflight) {
350
365
  this.cancel();
@@ -360,10 +375,15 @@ class Worker {
360
375
  wait();
361
376
  });
362
377
  }
363
- const result = await this._call('session/new', {
364
- cwd: this.cwd,
365
- mcpServers: mcpServers || [],
366
- });
378
+ // Bug B (issue #2479): if the caller is rotating the session because the
379
+ // system prompt changed, they may also be passing a fresh model/effort —
380
+ // update bookkeeping BEFORE session/new so the new fields land on the
381
+ // daemon. Falls through to whatever we already had when callers omit them.
382
+ if (model !== undefined) this.model = model;
383
+ if (effort !== undefined) this.effort = effort;
384
+ const result = await this._call('session/new', _buildSessionNewParams({
385
+ cwd: this.cwd, mcpServers: mcpServers || [], model: this.model, effort: this.effort,
386
+ }));
367
387
  this.sessionId = result && result.sessionId;
368
388
  this.systemPromptHash = systemPromptHash;
369
389
  this.firstSystemPromptSent = false;
@@ -425,9 +445,9 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
425
445
  } else if (worker.systemPromptHash !== systemPromptHash) {
426
446
  // System prompt changed → keep the warm process, drop the session
427
447
  // and create a fresh one. Saves the ~2.1 s initialize handshake.
428
- await worker.newSession({ mcpServers, systemPromptHash });
429
- worker.model = model;
430
- worker.effort = effort;
448
+ // Pass model/effort so the NEW session picks up any override the
449
+ // caller has rotated in (Bug B / issue #2479).
450
+ await worker.newSession({ mcpServers, systemPromptHash, model, effort });
431
451
  worker.lastUsedAt = _internals.now();
432
452
  } else {
433
453
  // Warm reuse — only update bookkeeping. model/effort changes on a
@@ -507,6 +527,7 @@ module.exports = {
507
527
  _internals,
508
528
  _tabs,
509
529
  _reapIdleTabs,
530
+ _buildSessionNewParams,
510
531
  IDLE_REAPER_MS,
511
532
  REAPER_INTERVAL_MS,
512
533
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-14T02:06:41.197Z"
4
+ "cachedAt": "2026-05-14T02:50:03.274Z"
5
5
  }
@@ -2,27 +2,51 @@
2
2
  * engine/features.js — Feature flag registry. Recipe in CLAUDE.md → "Feature Flags".
3
3
  */
4
4
 
5
- // Entry shape: id → { description, default: bool, addedIn?: version, expires?: ISO-date }
5
+ // Entry shape: id → { description, default: bool, addedIn?: version, expires?: ISO-date, requiredCcRuntime?: string }
6
6
  //
7
- // INTENTIONAL EMPTY REGISTRY — DO NOT DELETE.
7
+ // Optional fields:
8
+ // `requiredCcRuntime` (e.g. 'copilot') signals that the flag only takes
9
+ // effect when `resolveCcCli(engine)` matches. The dashboard uses this to
10
+ // render the toggle as disabled with a tooltip when the configured CC
11
+ // runtime is incompatible. Engine code still has to gate explicitly via
12
+ // `isFeatureOn` + a runtime check — `requiredCcRuntime` is a UX hint only.
13
+ //
14
+ // REGISTRY GROWTH POLICY — DO NOT DELETE THE FRAMEWORK.
8
15
  //
9
- // The `FEATURES = {}` literal below is intentional scaffolding, not dead code.
10
16
  // The surrounding framework (isFeatureOn, listFeatures, hasFeature, env-var
11
17
  // override resolver, expiration timestamps) is load-bearing: dashboard.js
12
18
  // boot wires it into /api/features (list), /api/features/toggle, and the
13
19
  // `window.MINIONS_FEATURES` client bootstrap. Deleting the registry — even
14
- // while empty — would break dashboard startup and the experimental-flags UI.
20
+ // if it became empty — would break dashboard startup and the experimental-flags UI.
15
21
  //
16
22
  // New flags belong here. Register them in this object instead of removing
17
- // the empty literal. The first real entry will replace the example below;
18
- // until then the empty object is the correct, expected shape.
23
+ // entries. Keep at least one entry so the framework remains demonstrably wired.
19
24
  //
20
25
  // Decision logged in the 2026-05-13 daily architecture & bug review meeting
21
26
  // (knowledge/architecture/2026-05-13-ripley-meeting-conclusion-daily-architecture-bug-review-2.md,
22
- // PR-C, Option B — keep framework, document the empty registry).
27
+ // PR-C, Option B — keep framework, document the registry).
23
28
  const FEATURES = {
24
- // Example:
25
- // 'ux-sidebar-v2': { description: '…', default: false, addedIn: '0.1.1738', expires: '2026-06-01' },
29
+ // ccUseWorkerPool — sub-tasks B/C/D of W-mp2w003600196c51 (CC perf).
30
+ // Routes Command Center / doc-chat through engine/cc-worker-pool.js
31
+ // (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI
32
+ // per turn. Saves ~14 s of cold-start cost. Copilot-only — the pool drives
33
+ // ACP which Claude does not implement.
34
+ //
35
+ // Resolution: `shared.resolveCcUseWorkerPool(engine)` (engine/shared.js)
36
+ // is the canonical predicate every dashboard.js call site uses. Explicit
37
+ // `engine.ccUseWorkerPool` true/false in config wins; otherwise the helper
38
+ // defaults ON for `copilot` and OFF for `claude`. PR #2492 flipped the
39
+ // default ON for copilot; see that PR for the cold-spawn measurements.
40
+ //
41
+ // `requiredCcRuntime: 'copilot'` here is a UX hint — the Settings panel
42
+ // greys out the toggle when the resolved CC runtime mismatches so users
43
+ // see the constraint instead of toggling a no-op.
44
+ 'ccUseWorkerPool': {
45
+ description: 'Route Command Center / doc-chat through a persistent `copilot --acp` worker per tab instead of spawning a fresh CLI per turn (~14s cold-start savings). Copilot-only — has no effect when the CC runtime is Claude.',
46
+ default: false,
47
+ addedIn: '0.1.1916',
48
+ requiredCcRuntime: 'copilot',
49
+ },
26
50
  };
27
51
 
28
52
  const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);
@@ -64,6 +88,7 @@ function listFeatures(config, registry = FEATURES) {
64
88
  addedIn: meta.addedIn || null,
65
89
  expires: meta.expires || null,
66
90
  expired: expiresAt < now,
91
+ requiredCcRuntime: meta.requiredCcRuntime || null,
67
92
  };
68
93
  });
69
94
  }
package/engine.js CHANGED
@@ -823,7 +823,9 @@ async function spawnAgent(dispatchItem, config) {
823
823
  shared.assertWorktreeOutsideProject(worktreePath, rootDir);
824
824
 
825
825
  // If branch is already checked out in an existing worktree, reuse it
826
+ _phaseT.findExistingStart = Date.now();
826
827
  const existingWt = await findExistingWorktree(rootDir, branchName);
828
+ _phaseT.findExistingEnd = Date.now();
827
829
  if (existingWt) {
828
830
  // Same guard for reuse — a previously-created bad worktree must not
829
831
  // be silently reused either; the cleanup sweep flags these so the
@@ -834,13 +836,16 @@ async function spawnAgent(dispatchItem, config) {
834
836
  // Probe origin first — locally-created branches that were never pushed
835
837
  // (orphan/timeout retry before first push) would otherwise emit a
836
838
  // "couldn't find remote ref" warn pair on every reuse.
839
+ _phaseT.reuseSyncStart = Date.now();
837
840
  await syncReusedWorktree(rootDir, existingWt, branchName, _gitOpts);
841
+ _phaseT.reuseSyncEnd = Date.now();
838
842
  } else if (READ_ONLY_ROOT_TASK_TYPES.has(type) && !isPipelineBranchName(branchName)) {
839
843
  // Read-only tasks — no worktree needed, run in rootDir
840
844
  log('info', `${type}: read-only task, no worktree needed — running in rootDir`);
841
845
  branchName = null;
842
846
  worktreePath = null;
843
847
  } else {
848
+ _phaseT.createWorktreeStart = Date.now();
844
849
  try {
845
850
  if (!fs.existsSync(worktreePath)) {
846
851
  const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
@@ -965,6 +970,7 @@ async function spawnAgent(dispatchItem, config) {
965
970
  return null;
966
971
  }
967
972
  }
973
+ _phaseT.createWorktreeEnd = Date.now();
968
974
  }
969
975
 
970
976
  // Shared-branch preflight (#2439): refuse to dispatch into a dirty shared
@@ -974,7 +980,9 @@ async function spawnAgent(dispatchItem, config) {
974
980
  // fires AFTER spawn — converting orchestration hygiene into fake
975
981
  // implementation failures and cascading dependent items.
976
982
  if (worktreePath && fs.existsSync(worktreePath) && meta?.branchStrategy === 'shared-branch' && branchName) {
983
+ _phaseT.cleanCheckStart = Date.now();
977
984
  const cleanResult = await assertCleanSharedWorktree(rootDir, worktreePath, branchName, id, _gitOpts);
985
+ _phaseT.cleanCheckEnd = Date.now();
978
986
  if (!cleanResult.clean) {
979
987
  const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
980
988
  const reasonMsg = `DIRTY_WORKTREE: shared branch ${branchName} worktree at ${worktreePath} is dirty (${cleanResult.reason}); ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}`;
@@ -1281,6 +1289,7 @@ async function spawnAgent(dispatchItem, config) {
1281
1289
  // Inject dirty file list when worktree has uncommitted changes (e.g., max_turns retry)
1282
1290
  // This signals to the respawned agent that prior work exists in the worktree (#960)
1283
1291
  if (worktreePath && fs.existsSync(worktreePath)) {
1292
+ _phaseT.dirtyProbeStart = Date.now();
1284
1293
  try {
1285
1294
  const dirtyResult = await execAsync('git status --porcelain', { ..._gitOpts, cwd: worktreePath, timeout: 10000 });
1286
1295
  const dirtyOutput = (dirtyResult.stdout || '').trim();
@@ -1298,6 +1307,7 @@ async function spawnAgent(dispatchItem, config) {
1298
1307
  log('info', `Injected ${dirtyFiles.length} dirty files into prompt for ${id}`);
1299
1308
  }
1300
1309
  } catch (e) { log('warn', `git status --porcelain for dirty files: ${e.message}`); }
1310
+ _phaseT.dirtyProbeEnd = Date.now();
1301
1311
  }
1302
1312
 
1303
1313
  // Safety check: warn if a write-capable task is running in the main repo without a worktree
@@ -1504,6 +1514,11 @@ async function spawnAgent(dispatchItem, config) {
1504
1514
  const timings = {
1505
1515
  prompt: _diff('start', 'afterPrompt'),
1506
1516
  worktree_total: _diff('afterPrompt', 'afterWorktree'),
1517
+ wt_find_existing: _diff('findExistingStart', 'findExistingEnd'),
1518
+ wt_reuse_sync: _diff('reuseSyncStart', 'reuseSyncEnd'),
1519
+ wt_create: _diff('createWorktreeStart', 'createWorktreeEnd'),
1520
+ wt_clean_check: _diff('cleanCheckStart', 'cleanCheckEnd'),
1521
+ wt_dirty_probe: _diff('dirtyProbeStart', 'dirtyProbeEnd'),
1507
1522
  dep_fetch: _diff('depFetchStart', 'depFetchEnd'),
1508
1523
  dep_preflight: _diff('depPreflightStart', 'depPreflightEnd'),
1509
1524
  dep_merge: _diff('depMergeStart', 'depMergeEnd'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1927",
3
+ "version": "0.1.1929",
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"