@yemi33/minions 0.1.2374 → 0.1.2376

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.
@@ -446,6 +446,7 @@ async function openSettings() {
446
446
  '<div class="settings-pane-sub">Persistent CC worker pool and git worktree provisioning. Tune these only if you see worktree-create timeouts or slow CC cold-spawns.</div>' +
447
447
  '<div class="settings-stack" style="margin-bottom:12px">' +
448
448
  settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.') +
449
+ settingsToggle('Agent Worker Pool', 'set-agentUseWorkerPool', e.agentUseWorkerPool !== false, 'Route fleet Copilot dispatches through persistent copilot --acp workers. Default ON; runtimes without ACP support ignore this setting.') +
449
450
  // W-mq6f2fe0000557fa — opt-in: auto-kill orphan spawn-agent processes
450
451
  // holding a stuck worktree's cwd. Gated by safe-reap criteria (cmdline
451
452
  // matches spawn-agent.js + basename + age > agentTimeout * 2). Default
@@ -484,12 +485,6 @@ async function openSettings() {
484
485
  '</select>' +
485
486
  '</div>' +
486
487
  settingsField('Copilot fallback model', 'set-copilotFallbackModel', e.copilotFallbackModel || '', 'e.g. gpt-5.4', 'Copilot has no --fallback-model flag. On a MODEL_UNAVAILABLE (overloaded/503) retry, the engine OVERRIDES --model with this value (Copilot only).') +
487
- '</div>' +
488
- '<div class="settings-stack" style="margin-top:12px">' +
489
- settingsField('Copilot: disable agent MCP servers', 'set-copilotAgentDisabledMcpServers',
490
- Array.isArray(e.copilotAgentDisabledMcpServers) ? e.copilotAgentDisabledMcpServers.join(', ') : (e.copilotAgentDisabledMcpServers || ''),
491
- 'e.g. playwright, maestro, loop',
492
- 'Comma-separated MCP server names (from ~/.copilot/mcp-config.json) the engine disables for autonomous Copilot agent dispatches by filtering them out of the agent-only COPILOT_HOME. Copilot loads your user MCP config on every spawn regardless of --add-dir; list the noisy/auth-popping servers (e.g. playwright, maestro) to keep them out of agents. Empty = inherit all.') +
493
488
  '</div>';
494
489
 
495
490
  const paneClaude =
@@ -588,7 +583,7 @@ async function openSettings() {
588
583
  '</div>' +
589
584
  '<h4>Agent Memory</h4>' +
590
585
  '<div class="settings-stack">' +
591
- settingsToggle('Shadow mode', 'set-memoryRetrievalShadowMode', e.memoryRetrievalShadowMode !== false, 'Measure retrieval without changing prompts') +
586
+ settingsToggle('Shadow mode', 'set-memoryRetrievalShadowMode', !!e.memoryRetrievalShadowMode, 'Measure retrieval without changing prompts') +
592
587
  settingsToggle('Capture task episodes', 'set-memoryEpisodicCapture', !!e.memoryEpisodicCapture, 'Store compact outcomes, never transcripts') +
593
588
  '</div>' +
594
589
  '<div class="settings-grid-2">' +
@@ -1128,6 +1123,7 @@ async function saveSettings() {
1128
1123
  autoFixHumanComments: document.getElementById('set-autoFixHumanComments').checked,
1129
1124
  autoCompletePrs: document.getElementById('set-autoCompletePrs').checked,
1130
1125
  ccUseWorkerPool: !!document.getElementById('set-ccUseWorkerPool')?.checked,
1126
+ agentUseWorkerPool: !!document.getElementById('set-agentUseWorkerPool')?.checked,
1131
1127
  autoReapOrphanWorktreeHolders: !!document.getElementById('set-autoReapOrphanWorktreeHolders')?.checked,
1132
1128
  liveCheckoutAutoStash: !!document.getElementById('set-liveCheckoutAutoStash')?.checked,
1133
1129
  liveCheckoutAutoReset: !!document.getElementById('set-liveCheckoutAutoReset')?.checked,
@@ -1188,7 +1184,6 @@ async function saveSettings() {
1188
1184
  claudeBareMode: !!document.getElementById('set-claudeBareMode')?.checked,
1189
1185
  claudeFallbackModel: (document.getElementById('set-claudeFallbackModel')?.value ?? '').trim(),
1190
1186
  copilotFallbackModel: (document.getElementById('set-copilotFallbackModel')?.value ?? '').trim(),
1191
- copilotAgentDisabledMcpServers: (document.getElementById('set-copilotAgentDisabledMcpServers')?.value ?? '').trim(),
1192
1187
  copilotDisableBuiltinMcps: !!document.getElementById('set-copilotDisableBuiltinMcps')?.checked,
1193
1188
  copilotStreamMode: document.getElementById('set-copilotStreamMode')?.value || 'on',
1194
1189
  copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
package/dashboard.js CHANGED
@@ -11374,17 +11374,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11374
11374
  else if (valid.includes(e.copilotStreamMode)) _setEngineConfig('copilotStreamMode', e.copilotStreamMode);
11375
11375
  else _clamped.push(`copilotStreamMode: "${e.copilotStreamMode}" not in [on, off] (kept previous value)`);
11376
11376
  }
11377
- // P-mcp-storm — MCP server names to disable for autonomous Copilot agents
11378
- // (--disable-mcp-server). Accept an array OR a comma/whitespace string;
11379
- // normalize to a deduped array of trimmed names. Empty clears (inherit all).
11380
- if (e.copilotAgentDisabledMcpServers !== undefined) {
11381
- const src = Array.isArray(e.copilotAgentDisabledMcpServers)
11382
- ? e.copilotAgentDisabledMcpServers
11383
- : String(e.copilotAgentDisabledMcpServers || '').split(/[\s,]+/);
11384
- const names = [...new Set(src.map(s => String(s == null ? '' : s).trim()).filter(Boolean))];
11385
- if (names.length) _setEngineConfig('copilotAgentDisabledMcpServers', names);
11386
- else _deleteEngineConfig('copilotAgentDisabledMcpServers');
11387
- }
11388
11377
  // W-mpmwxkrw000872ec — fontSize allowlist. Clamps invalid values
11389
11378
  // (rather than silently failing) so the dashboard bootstrap never
11390
11379
  // ends up with an unknown data-font-size attribute.
@@ -15099,18 +15088,6 @@ if (require.main === module) {
15099
15088
  // engine reads, port binding) is captured rather than dying silently.
15100
15089
  _installCrashHandlers();
15101
15090
 
15102
- // Deliberately do NOT set process.env.COPILOT_HOME here. Command Center,
15103
- // doc-chat, and the CC worker pool (`copilot --acp`) all spawn `copilot`
15104
- // as this process's children, so they should inherit the operator's real
15105
- // `~/.copilot` home (including their model preference in settings.json)
15106
- // exactly like an interactive `copilot` CLI session would — that's the
15107
- // documented CLI contract. An earlier revision forced these onto an
15108
- // isolated, MCP-filtered home to dodge MCP-server auth popups, but that
15109
- // broke CC's settings inheritance and reintroduced a staleness/reseed bug;
15110
- // reverted (W-mre75l6x00024f49). Per-dispatch agent isolation is unaffected
15111
- // — the engine process still seeds its own COPILOT_HOME at boot and
15112
- // re-stamps it per spawn via `_applyAgentCopilotHome` (engine.js).
15113
-
15114
15091
  // Kick off first npm check on startup, then re-check every 4 hours.
15115
15092
  // Guarded here so that requiring dashboard as a module (unit tests, _withDashServer)
15116
15093
  // does not spawn npm child processes that keep the event loop alive past test teardown
@@ -11,6 +11,7 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
11
11
 
12
12
  - `discoverFromPrs()` (engine.js) runs each tick (~10s)
13
13
  - Gates: `status === 'active'` + `reviewStatus === 'pending'` + not reviewed since last push + not dispatched + not on cooldown
14
+ - "Not reviewed since last push" is `!isPrReviewCurrent(pr)` — compares `pr.reviewedHeadSha` (the SHA the completed review verdict actually evaluated) against the live head, falling back to the legacy `lastPushedAt <= lastReviewedAt` timestamp comparison for PR records that predate `reviewedHeadSha`. The SHA-based check replaces a pure timestamp comparison, which could false-positive as "already reviewed" when a commit landed mid-review (`lastReviewedAt` is stamped at completion, i.e. *after* the mid-review push) — root cause of github:opg-microsoft/minions#821. Re-review (`reviewStatus === 'waiting'`) uses the mirrored `isPrFixedAfterReview(pr)` gate for the same reason. Both dispatch sites capture `reviewHeadSha: getPrCauseHead(pr)` into dispatch `meta` so the eventual verdict can stamp the exact SHA it evaluated.
14
15
  - Pre-dispatch: `checkLiveReviewStatus()` hits GitHub/ADO API to catch stale cached status
15
16
  - Routes to reviewer via `resolveAgent('review')`, dispatches with `review.md` playbook
16
17
 
@@ -144,8 +145,9 @@ A "branch unchanged" no-op report for a `BUILD_FAILURE`-cause fix used to be con
144
145
  | `buildErrorLog` | Poller | Actual CI error output for fix agents |
145
146
  | `_buildFixPushedAt` | Discovery (on dispatch) | Grace period timestamp |
146
147
  | `_buildFailNotified` | Discovery | Dedup for inbox alert |
147
- | `lastPushedAt` | Poller (new commit) | Tracks latest push for re-review logic |
148
- | `lastReviewedAt` | `updatePrAfterReview()` | Prevents re-dispatch if reviewed |
148
+ | `lastPushedAt` | Poller (new commit) | Tracks latest push for re-review logic (legacy fallback signal) |
149
+ | `lastReviewedAt` | `updatePrAfterReview()` | Legacy fallback for freshness gating on PR records that predate `reviewedHeadSha` |
150
+ | `reviewedHeadSha` | `updatePrAfterReview()` | SHA the completed review verdict actually evaluated (captured at dispatch time via `meta.reviewHeadSha`, or falls back to the live head for legacy in-flight dispatches); primary review-freshness signal (`isPrReviewCurrent`/`isPrFixedAfterReview`) |
149
151
  | `minionsReview` | Post-completion hooks | `{ reviewer, reviewedAt, note, fixedAt }` |
150
152
  | `humanFeedback` | `pollPrHumanComments()` | `{ pendingFix, feedbackContent, lastProcessedCommentDate }` |
151
153
  | `_noOpFixes[cause]` | `recordPrNoOpFixAttempt()` | Per-cause record `{ count, paused, fingerprint, beforeHead, afterHead }` driving the issue #2969 pause loop |
@@ -321,8 +321,6 @@ means zero such files were found), **Proposed target module**.
321
321
  | `resolveCcModel` | 3935-3950 | Resolve the model for the Command Center / doc-chat. Priority: 1. `engine.ccModel` — CC-only override 2. `engine.defaultModel` — fleet default (CC inherits this when ccModel unset) 3. `undefined` — let the runtime adapte | yes | `engine/llm.js`, `engine/pre-dispatch-eval.js`, `test/unit/auto-recovery.test.js`, `test/unit.test.js`, `dashboard.js` | engine/shared.js (keep) |
322
322
  | `resolveAgentMaxBudget` | 3951-3973 | Resolve the per-spawn USD budget cap. Priority: 1. `agent.maxBudgetUsd` — per-agent override 2. `engine.maxBudgetUsd` — fleet default 3. `undefined` — no cap Uses nullish coalescing so a literal `0` is honored as a valid | yes | `test/unit.test.js`, `engine.js` | engine/shared.js (keep) |
323
323
  | `resolveAgentBareMode` | 3974-3991 | Resolve whether this agent should run in Claude `--bare` mode. Priority: 1. `agent.bareMode` — per-agent override (boolean) 2. `engine.claudeBareMode` — fleet default 3. `false` — hardcoded fallback Strict undefined/null | yes | `test/unit.test.js`, `engine.js` | engine/shared.js (keep) |
324
- | `_normalizeMcpServerList` | 3992-4005 | P-mcp-storm — Resolve the list of MCP server names to disable for spawned // Copilot agents. The engine FILTERS these out of the agent's seeded // COPILOT_HOME mcp-config (engine.js `_applyAgentCopilotHome`) — a server a | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
325
- | `resolveCopilotAgentDisabledMcpServers` | 4006-4030 | Resolve copilot agent disabled mcp servers. | yes | `test/unit/agent-copilot-home.test.js` | engine/shared.js (keep) |
326
324
  | `applyLegacyCcModelMigration` | 4063-4082 | If `config.engine.ccModel` is set but `config.engine.defaultModel` is unset, mutate the in-memory `config.engine` so `defaultModel` mirrors `ccModel` and log a one-time deprecation notice. Does NOT write to disk. Returns | yes | `engine/cli.js`, `test/unit.test.js` | engine/shared.js (keep) |
327
325
  | `_resetLegacyCcModelMigrationFlag` | 4083-4113 | Test helper: reset the dedup flag so repeated tests can re-trigger the log. | yes | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
328
326
  | `runtimeConfigWarnings` | 4114-4194 | Inspect runtime fleet config and return warning entries for misconfiguration. Warnings emitted: - Unknown CLI: any `cli` value (per-agent, ccCli, defaultCli) not in `registeredRuntimes`. Each unknown value produces one e | yes | `engine/preflight.js` | engine/process-utils.js (new) — process spawn/pid/tmp-dir lifecycle helpers |
@@ -12,7 +12,7 @@ Multiple agents work the same repository in parallel and across days. Without sh
12
12
  - Agents re-litigate decisions the team already made (drift, regressions).
13
13
  - Human-flagged warnings (e.g., "do not self-approve PRs") get ignored because they live outside the prompt.
14
14
 
15
- Team memory closes the loop. The engine always injects pinned context. By default it also injects recent team and personal notes; when structured retrieval is enabled outside shadow mode, it replaces those broad appendices with a bounded task-relevant memory pack. Playbooks still instruct agents to consult `knowledge/` and `notes/inbox/` on disk before going wider.
15
+ Team memory closes the loop. The engine always injects pinned context. By default, structured retrieval replaces broad team and personal notes with a bounded task-relevant memory pack. Playbooks still instruct agents to consult `knowledge/` and `notes/inbox/` on disk before going wider.
16
16
 
17
17
  ## Structured memory and retrieval
18
18
 
@@ -20,11 +20,11 @@ Migration `017-agent-memory.js` backfills the existing Markdown sources into `me
20
20
 
21
21
  `engine/memory-retrieval.js` searches active records in global, current-project, and assigned-agent scopes. It reranks lexical matches by scope, trust, path match, recency, importance, and confidence, then packs the best records into a strict byte budget. Agent- or external-authored procedural records are never recalled as instructions. Every injected pack remains inside an `<UNTRUSTED-INPUT>` fence.
22
22
 
23
- Rollout is controlled by the default-off `memoryRetrieval` feature flag:
23
+ Structured retrieval is controlled by the default-on `memoryRetrieval` feature flag:
24
24
 
25
25
  - **Flag off:** legacy prompt behavior.
26
- - **Flag on + `memoryRetrievalShadowMode: true` (default):** compute retrieval and telemetry, but keep legacy prompts.
27
- - **Flag on + shadow mode off:** inject `Relevant Memory` when retrieval finds evidence; otherwise fall back to legacy notes and personal memory.
26
+ - **Flag on + shadow mode off (default):** inject `Relevant Memory` when retrieval finds evidence; otherwise fall back to legacy notes and personal memory.
27
+ - **Flag on + `memoryRetrievalShadowMode: true`:** compute retrieval and telemetry, but keep legacy prompts.
28
28
 
29
29
  Settings expose top-k, candidate count, byte budget, shadow mode, and episodic capture. The Inbox page's **Memory search** control shows provenance and status and supports explicit pin, retract, and restore actions.
30
30
 
@@ -96,8 +96,8 @@ const FEATURES = {
96
96
  expires: '2026-12-01',
97
97
  },
98
98
  'memoryRetrieval': {
99
- description: 'Select bounded task-relevant long-term memory with SQLite FTS5 instead of injecting only recent team and personal notes. Starts in shadow mode by default.',
100
- default: false,
99
+ description: 'Select bounded task-relevant long-term memory with SQLite FTS5 instead of injecting broad team and personal notes.',
100
+ default: true,
101
101
  addedIn: '0.1.2233',
102
102
  expires: '2027-01-31',
103
103
  },
package/engine/github.js CHANGED
@@ -99,6 +99,28 @@ function getRepoSlug(project) {
99
99
  return `${org}/${repo}`;
100
100
  }
101
101
 
102
+ function getPrRepoSlug(project, pr) {
103
+ const scope = shared.getPrScopeInfo(pr, pr?.url || '')?.scope || '';
104
+ if (scope && scope === shared.getProjectPrScope(project)) return getRepoSlug(project);
105
+ if (scope.startsWith('github:')) return scope.slice('github:'.length);
106
+ return getRepoSlug(project);
107
+ }
108
+
109
+ function getProjectRepoSlugs(project) {
110
+ const primary = getRepoSlug(project);
111
+ const slugs = primary ? [primary] : [];
112
+ const seen = new Set(slugs.map(slug => slug.toLowerCase()));
113
+ for (const scope of shared.getProjectAllPrScopes(project)) {
114
+ if (!scope.startsWith('github:')) continue;
115
+ const slug = scope.slice('github:'.length);
116
+ if (!seen.has(slug)) {
117
+ slugs.push(slug);
118
+ seen.add(slug);
119
+ }
120
+ }
121
+ return slugs;
122
+ }
123
+
102
124
  function getConfiguredGitHubAuthorLogins(config = {}) {
103
125
  const accounts = config?.engine?.ghAccounts;
104
126
  if (!accounts || typeof accounts !== 'object') return new Set();
@@ -520,40 +542,40 @@ async function forEachActiveGhPr(config, callback) {
520
542
  const src = project?.workSources?.pullRequests || config?.workSources?.pullRequests;
521
543
  if (src && src.enabled === false) continue;
522
544
 
523
- const slug = getRepoSlug(project);
524
- if (!slug) continue;
525
-
526
- // Skip projects in backoff (inaccessible repo)
527
- if (isSlugInBackoff(slug)) continue;
528
-
529
545
  const prs = getPrs(project);
530
546
  const activePrs = prs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status)
531
547
  && shared.isPrCompatibleWithProject(project, pr, pr.url || ''));
532
548
  if (activePrs.length === 0) continue;
533
549
 
534
- // Probe repo accessibility before iterating PRs — avoids N warnings per inaccessible repo.
535
- // W-mp5trwh60008386d: ghApi returns the GH_NOT_FOUND sentinel on 404 (a frozen object,
536
- // *not* null). The pre-fix gate only matched `null`, so a 404 on the base repo (caused by
537
- // a multi-account `gh auth` switch, network blip, or token rotation) fell through and every
538
- // per-PR call below 404'd, permanently flipping all active PRs to `abandoned`. We now treat
539
- // both null and GH_NOT_FOUND as "skip the project for this tick" and explicitly do NOT
540
- // increment per-PR `_consecutive404s` counters since no per-PR call was made.
541
- const probe = await ghApi('', slug);
542
- if (probe === null || probe === GH_NOT_FOUND) {
543
- if (probe === GH_NOT_FOUND) {
544
- log('warn', `GitHub repo probe for ${slug} returned 404 — skipping all per-PR polls for this project this tick (avoids spurious abandonments). Per-PR 404 counters NOT incremented.`);
545
- }
546
- recordSlugFailure(slug);
547
- continue;
548
- }
549
- resetSlugBackoff(slug);
550
-
551
550
  let projectUpdated = 0;
552
551
  const updatedRecords = [];
552
+ const slugProbes = new Map();
553
553
 
554
554
  for (const pr of activePrs) {
555
555
  const prNum = shared.getPrNumber(pr);
556
556
  if (!prNum) continue;
557
+ const slug = getPrRepoSlug(project, pr);
558
+ if (!slug) continue;
559
+
560
+ let probeResult = slugProbes.get(slug);
561
+ if (probeResult === undefined) {
562
+ if (isSlugInBackoff(slug)) {
563
+ probeResult = 'fail';
564
+ } else {
565
+ const probe = await ghApi('', slug);
566
+ probeResult = (probe === null || probe === GH_NOT_FOUND) ? 'fail' : 'ok';
567
+ if (probeResult === 'fail') {
568
+ if (probe === GH_NOT_FOUND) {
569
+ log('warn', `GitHub repo probe for ${slug} returned 404 — skipping all per-PR polls for this repository this tick (avoids spurious abandonments). Per-PR 404 counters NOT incremented.`);
570
+ }
571
+ recordSlugFailure(slug);
572
+ } else {
573
+ resetSlugBackoff(slug);
574
+ }
575
+ }
576
+ slugProbes.set(slug, probeResult);
577
+ }
578
+ if (probeResult !== 'ok') continue;
557
579
 
558
580
  try {
559
581
  const before = shared.snapshotPrRecord(pr);
@@ -612,7 +634,10 @@ async function forEachActiveGhPr(config, callback) {
612
634
  const centralPrs = safeJsonArr(centralPath);
613
635
  // Build a slug→project map for configured GitHub projects so we can detect
614
636
  // PRs that are actually owned by the project loop (present in per-project file).
615
- const configuredGhProjectsBySlug = new Map(projects.map(p => [getRepoSlug(p), p]));
637
+ const configuredGhProjectsBySlug = new Map();
638
+ for (const project of projects) {
639
+ for (const slug of getProjectRepoSlugs(project)) configuredGhProjectsBySlug.set(slug.toLowerCase(), project);
640
+ }
616
641
  const activeCentral = centralPrs.filter(pr => {
617
642
  if (!PR_POLLABLE_STATUSES.has(pr.status)) return false;
618
643
  if (!pr.url) return false;
@@ -621,7 +646,7 @@ async function forEachActiveGhPr(config, callback) {
621
646
  // the project loop will never see it — the central branch must poll it.
622
647
  const ghMatch = pr.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/);
623
648
  if (ghMatch) {
624
- const owningProject = configuredGhProjectsBySlug.get(ghMatch[1]);
649
+ const owningProject = configuredGhProjectsBySlug.get(ghMatch[1].toLowerCase());
625
650
  if (owningProject) {
626
651
  const projectPrs = safeJsonArr(projectPrPath(owningProject));
627
652
  if (projectPrs.some(p => p.id === pr.id)) return false; // present → project loop handles it
@@ -1425,12 +1450,6 @@ async function reconcilePrs(config) {
1425
1450
  let totalAdded = 0;
1426
1451
 
1427
1452
  for (const project of projects) {
1428
- const slug = getRepoSlug(project);
1429
- if (!slug) continue;
1430
-
1431
- // Skip projects in backoff (inaccessible repo)
1432
- if (isSlugInBackoff(slug)) continue;
1433
-
1434
1453
  // Skip projects with no tracked PRs and no work items — nothing to reconcile
1435
1454
  const existingPrs = getPrs(project);
1436
1455
  if (existingPrs.length === 0) {
@@ -1441,18 +1460,21 @@ async function reconcilePrs(config) {
1441
1460
  } catch { continue; }
1442
1461
  }
1443
1462
 
1444
- // Fetch open PRs — paginate to handle repos with >100 open PRs
1445
- const prsData = await ghApi('/pulls?state=open&per_page=100', slug, { paginate: true, timeout: 60000 });
1446
- if (!prsData || !Array.isArray(prsData)) {
1447
- recordSlugFailure(slug);
1448
- continue;
1463
+ const ghPrs = [];
1464
+ for (const slug of getProjectRepoSlugs(project)) {
1465
+ if (isSlugInBackoff(slug)) continue;
1466
+ // Fetch open PRs — paginate to handle repos with >100 open PRs.
1467
+ const prsData = await ghApi('/pulls?state=open&per_page=100', slug, { paginate: true, timeout: 60000 });
1468
+ if (!prsData || !Array.isArray(prsData)) {
1469
+ recordSlugFailure(slug);
1470
+ continue;
1471
+ }
1472
+ resetSlugBackoff(slug);
1473
+ for (const pr of prsData) {
1474
+ const branch = pr.head?.ref || '';
1475
+ if (branchPatterns.some(pat => pat.test(branch))) ghPrs.push({ pr, slug });
1476
+ }
1449
1477
  }
1450
- resetSlugBackoff(slug);
1451
-
1452
- const ghPrs = prsData.filter(pr => {
1453
- const branch = pr.head?.ref || '';
1454
- return branchPatterns.some(pat => pat.test(branch));
1455
- });
1456
1478
 
1457
1479
  if (ghPrs.length === 0) continue;
1458
1480
 
@@ -1470,8 +1492,12 @@ async function reconcilePrs(config) {
1470
1492
  const centralItems = safeJsonArr(centralWiPath);
1471
1493
  const allItems = [...workItems, ...centralItems];
1472
1494
 
1473
- for (const ghPr of ghPrs) {
1474
- const prUrl = project.prUrlBase ? project.prUrlBase + ghPr.number : ghPr.html_url || '';
1495
+ for (const { pr: ghPr, slug } of ghPrs) {
1496
+ const primarySlug = getRepoSlug(project);
1497
+ const prUrl = ghPr.html_url
1498
+ || (slug.toLowerCase() === primarySlug?.toLowerCase() && project.prUrlBase
1499
+ ? project.prUrlBase + ghPr.number
1500
+ : `https://github.com/${slug}/pull/${ghPr.number}`);
1475
1501
  const prId = shared.getCanonicalPrId(project, ghPr.number, prUrl);
1476
1502
  // P-a7c4d2e8 (F3): validate API-derived branch ref before persistence
1477
1503
  // / regex matching. Defensive degrade — log and treat as missing on
@@ -1604,7 +1630,7 @@ async function reconcilePrs(config) {
1604
1630
  */
1605
1631
  async function checkLiveReviewStatus(pr, project) {
1606
1632
  try {
1607
- const slug = getRepoSlug(project);
1633
+ const slug = getPrRepoSlug(project, pr);
1608
1634
  if (!slug) return null;
1609
1635
  const prNum = shared.getPrNumber(pr);
1610
1636
  if (!prNum) return null;
@@ -1654,7 +1680,7 @@ async function checkLiveReviewStatus(pr, project) {
1654
1680
  async function dismissPriorViewerChangesRequestedReviews(pr, project) {
1655
1681
  let tmpFile = null;
1656
1682
  try {
1657
- const slug = getRepoSlug(project);
1683
+ const slug = getPrRepoSlug(project, pr);
1658
1684
  if (!slug) return null;
1659
1685
  const prNum = shared.getPrNumber(pr);
1660
1686
  if (!prNum) return null;
@@ -1758,7 +1784,7 @@ async function dismissPriorViewerChangesRequestedReviews(pr, project) {
1758
1784
  */
1759
1785
  async function checkLiveBuildAndConflict(pr, project) {
1760
1786
  try {
1761
- const slug = getRepoSlug(project);
1787
+ const slug = getPrRepoSlug(project, pr);
1762
1788
  if (!slug) return null;
1763
1789
  const prNum = shared.getPrNumber(pr);
1764
1790
  if (!prNum) return null;
@@ -1846,9 +1872,6 @@ async function reconcileAbandonedPrs(config) {
1846
1872
  const slugProbeCache = new Map(); // slug → 'ok' | 'fail'
1847
1873
 
1848
1874
  for (const project of projects) {
1849
- const slug = getRepoSlug(project);
1850
- if (!slug) continue;
1851
-
1852
1875
  const prPath = projectPrPath(project);
1853
1876
  const prs = safeJsonArr(prPath);
1854
1877
  // Filter: only abandoned PRs that don't already have the confirmed-404
@@ -1862,37 +1885,35 @@ async function reconcileAbandonedPrs(config) {
1862
1885
  );
1863
1886
  if (abandonedPrs.length === 0) continue;
1864
1887
 
1865
- // Probe base repo (cached). Failure skip ALL of this slug's abandoned
1866
- // PRs and increment skipped counter — auth/access issue at boot, retry
1867
- // next restart.
1868
- let probeResult = slugProbeCache.get(slug);
1869
- if (probeResult === undefined) {
1870
- const probe = await ghApi('', slug);
1871
- probeResult = (probe === null || probe === GH_NOT_FOUND) ? 'fail' : 'ok';
1872
- slugProbeCache.set(slug, probeResult);
1873
- }
1874
- if (probeResult === 'fail') {
1875
- log('warn', `Abandoned PR reconciliation: skipping ${slug} (${abandonedPrs.length} PR${abandonedPrs.length === 1 ? '' : 's'}) — base-repo probe failed, retry next startup`);
1876
- skipped += abandonedPrs.length;
1877
- continue;
1878
- }
1879
-
1880
- // Per-PR re-probe. Collect updates first, then apply via mutatePullRequests
1881
- // for atomic single-writer semantics. We match by prNumber on writeback
1882
- // (not id) because mutatePullRequests calls normalizePrRecords which can
1883
- // lowercase the id slug — prNumber is the stable key within a project's
1884
- // pull-requests.json.
1885
- const updates = []; // { prNumber, action, newStatus?, mergedAt? }
1888
+ // Per-PR re-probe. Canonical ids keep same-number PRs from different
1889
+ // repository scopes isolated during writeback.
1890
+ const updates = []; // { prId, prNumber, action, newStatus?, mergedAt? }
1886
1891
  for (const pr of abandonedPrs) {
1887
1892
  const prNum = shared.getPrNumber(pr);
1888
1893
  if (!prNum) continue;
1894
+ const slug = getPrRepoSlug(project, pr);
1895
+ if (!slug) continue;
1896
+ const prId = shared.getCanonicalPrId(project, pr, pr.url || '');
1897
+ if (!prId) continue;
1898
+
1899
+ let probeResult = slugProbeCache.get(slug);
1900
+ if (probeResult === undefined) {
1901
+ const probe = await ghApi('', slug);
1902
+ probeResult = (probe === null || probe === GH_NOT_FOUND) ? 'fail' : 'ok';
1903
+ slugProbeCache.set(slug, probeResult);
1904
+ }
1905
+ if (probeResult === 'fail') {
1906
+ log('warn', `Abandoned PR reconciliation: skipping ${slug} — base-repo probe failed, retry next startup`);
1907
+ skipped++;
1908
+ continue;
1909
+ }
1889
1910
 
1890
1911
  try {
1891
1912
  const prData = await ghApi(`/pulls/${prNum}`, slug);
1892
1913
  if (prData === GH_NOT_FOUND) {
1893
1914
  // 404 with base-probe OK → genuinely deleted. Mark so we don't
1894
1915
  // re-probe this PR on future startups.
1895
- updates.push({ prNumber: prNum, action: 'confirm404' });
1916
+ updates.push({ prId, prNumber: prNum, action: 'confirm404' });
1896
1917
  confirmedDeleted++;
1897
1918
  log('info', `Confirmed PR #${prNum} (${slug}): truly deleted, leaving abandoned`);
1898
1919
  } else if (prData) {
@@ -1915,6 +1936,7 @@ async function reconcileAbandonedPrs(config) {
1915
1936
  continue;
1916
1937
  }
1917
1938
  updates.push({
1939
+ prId,
1918
1940
  prNumber: prNum,
1919
1941
  action: 'flip',
1920
1942
  newStatus,
@@ -1938,7 +1960,9 @@ async function reconcileAbandonedPrs(config) {
1938
1960
  const reconciledAt = ts();
1939
1961
  mutatePullRequests(prPath, (currentPrs) => {
1940
1962
  for (const upd of updates) {
1941
- const pr = currentPrs.find(p => shared.getPrNumber(p) === upd.prNumber);
1963
+ const pr = currentPrs.find(p =>
1964
+ shared.getCanonicalPrId(project, p, p.url || '') === upd.prId
1965
+ );
1942
1966
  if (!pr) continue;
1943
1967
  // Defensive: never downgrade a merged record. Should already be
1944
1968
  // filtered by the abandoned-only scan above, but a concurrent writer
@@ -9,6 +9,17 @@
9
9
  const crypto = require('node:crypto');
10
10
  const { getDb, withTransaction } = require('./db');
11
11
 
12
+ // Per CLAUDE.md's "State storage" mutator contract, every mutator wrapper
13
+ // must call emitStateEvent() after a successful write so the dashboard's
14
+ // MAX(events.id) cache-version advances — otherwise the dashboard keeps
15
+ // serving stale cached inbox state until an unrelated event happens to
16
+ // bump the counter. Best-effort: never let event emission fail a write.
17
+ function _emitInboxEvent(payload) {
18
+ try {
19
+ require('./db-events').emitStateEvent('inbox', payload);
20
+ } catch { /* best-effort */ }
21
+ }
22
+
12
23
  // In-process monotonic counter. Millisecond timestamps are too coarse to
13
24
  // disambiguate bursts of createInboxEntry() calls for the same
14
25
  // recipient+sender pair (e.g. rapid consolidation/notification writes), and
@@ -126,6 +137,7 @@ function createInboxEntry({
126
137
  }
127
138
  }
128
139
 
140
+ _emitInboxEvent({ id, recipientSessionId, action: 'create' });
129
141
  return getInboxEntryById(id);
130
142
  });
131
143
  }
@@ -144,6 +156,7 @@ function markInboxEntriesAsRead(entryIds) {
144
156
  const query = `UPDATE inbox_entries SET unread = 0, read_at = ? WHERE id IN (${placeholders})`;
145
157
  const stmt = db.prepare(query);
146
158
  const result = stmt.run(now, ...entryIds);
159
+ if (result.changes > 0) _emitInboxEvent({ entryIds, action: 'mark_read' });
147
160
  return { wrote: result.changes > 0 };
148
161
  });
149
162
  }
@@ -161,6 +174,7 @@ function markInboxEntriesAsUnread(entryIds) {
161
174
  const query = `UPDATE inbox_entries SET unread = 1, read_at = NULL WHERE id IN (${placeholders})`;
162
175
  const stmt = db.prepare(query);
163
176
  const result = stmt.run(...entryIds);
177
+ if (result.changes > 0) _emitInboxEvent({ entryIds, action: 'mark_unread' });
164
178
  return { wrote: result.changes > 0 };
165
179
  });
166
180
  }
@@ -176,6 +190,7 @@ function markAllInboxAsRead(recipientSessionId) {
176
190
  const result = db.prepare(
177
191
  'UPDATE inbox_entries SET unread = 0, read_at = ? WHERE recipient_session_id = ? AND unread = 1'
178
192
  ).run(now, recipientSessionId);
193
+ if (result.changes > 0) _emitInboxEvent({ recipientSessionId, action: 'mark_all_read' });
179
194
  return { wrote: result.changes > 0 };
180
195
  });
181
196
  }
@@ -188,6 +203,7 @@ function deleteInboxEntry(id) {
188
203
 
189
204
  return withTransaction(db, () => {
190
205
  const result = db.prepare('DELETE FROM inbox_entries WHERE id = ?').run(id);
206
+ if (result.changes > 0) _emitInboxEvent({ id, action: 'delete' });
191
207
  return { wrote: result.changes > 0 };
192
208
  });
193
209
  }
@@ -204,6 +220,7 @@ function deleteInboxEntries(entryIds) {
204
220
  const placeholders = entryIds.map(() => '?').join(',');
205
221
  const query = `DELETE FROM inbox_entries WHERE id IN (${placeholders})`;
206
222
  const result = db.prepare(query).run(...entryIds);
223
+ if (result.changes > 0) _emitInboxEvent({ entryIds, action: 'delete_batch' });
207
224
  return { wrote: result.changes > 0 };
208
225
  });
209
226
  }
@@ -222,6 +239,7 @@ function deleteInboxEntriesOlderThan(timestampMs, { unreadOnly = false } = {}) {
222
239
  query += ' AND unread = 1';
223
240
  }
224
241
  const result = db.prepare(query).run(...params);
242
+ if (result.changes > 0) _emitInboxEvent({ timestampMs, unreadOnly, action: 'delete_older_than' });
225
243
  return { wrote: result.changes > 0, deletedCount: result.changes };
226
244
  });
227
245
  }
@@ -252,6 +270,7 @@ function markInboxEntryNotified(id) {
252
270
  return withTransaction(db, () => {
253
271
  const now = Date.now();
254
272
  const result = db.prepare('UPDATE inbox_entries SET notified_at = ? WHERE id = ?').run(now, id);
273
+ if (result.changes > 0) _emitInboxEvent({ id, action: 'mark_notified' });
255
274
  return { wrote: result.changes > 0 };
256
275
  });
257
276
  }
package/engine/shared.js CHANGED
@@ -3485,25 +3485,8 @@ const ENGINE_DEFAULTS = {
3485
3485
  claudePreApproveWorkspaceMcps: true,
3486
3486
  copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
3487
3487
  copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
3488
- ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true AND CC runtime is copilot, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. **Structurally copilot-only**: the pool spawns `copilot --acp` (Agent Client Protocol); Claude Code does not implement ACP, so resolveCcUseWorkerPool returns false on non-copilot CC runtimes even with explicit-true (W-mphlriic00095f69 — prevents silent runtime switch). Engine/agent dispatch path stays per-process regardless.
3488
+ ccUseWorkerPool: true, // Sub-task C of W-mp2w003600196c51 (CC perf): when true AND CC runtime is copilot, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. On by default (PR #2492 cold-spawn measurements; matches engine/features.js's `ccUseWorkerPool` registry default) — opt out via `engine.ccUseWorkerPool: false`. **Structurally copilot-only**: the pool spawns `copilot --acp` (Agent Client Protocol); Claude Code does not implement ACP, so resolveCcUseWorkerPool returns false on non-copilot CC runtimes even with explicit-true (W-mphlriic00095f69 — prevents silent runtime switch). Engine/agent dispatch path stays per-process regardless.
3489
3489
  agentUseWorkerPool: false, // P-9b3d5f61: mirrors ccUseWorkerPool's shape but for the fleet agent-dispatch path (engine/agent-worker-pool.js). Opt-in, default OFF — see resolveAgentUseWorkerPool in engine/shared.js for the full priority chain and rationale.
3490
- // PR #321 — "kill auth-popup storm". Copilot spawns and authenticates EVERY
3491
- // MCP server in `$COPILOT_HOME/mcp-config.json` on every process start, even
3492
- // when the corresponding tool is hidden from the model via
3493
- // `--disable-mcp-server`. If left unfiltered, every auth-requiring user MCP
3494
- // (azure-kusto/azmcp, DevBox, workiq, loop, teams, …) boots and pops a
3495
- // Microsoft sign-in window on EVERY agent spawn — at minions' continuous
3496
- // spawn cadence that is an infinite popup storm.
3497
- //
3498
- // The engine therefore points agents at a private COPILOT_HOME
3499
- // (`shared.resolveAgentCopilotHome`) whose mcp-config is a copy of the operator's
3500
- // `~/.copilot/mcp-config.json` MINUS the names in this list (see engine.js
3501
- // `_applyAgentCopilotHome`). Default [] = inherit ALL of the operator's MCP
3502
- // servers for agents (non-breaking). Add a name to disable it for agents only;
3503
- // the operator's interactive `~/.copilot` is never touched. Per-agent override
3504
- // `agent.copilotAgentDisabledMcpServers`. Resolved through
3505
- // `shared.resolveCopilotAgentDisabledMcpServers(agent, engine)`.
3506
- copilotAgentDisabledMcpServers: [],
3507
3490
  maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
3508
3491
  disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
3509
3492
  // Phase 8 (qa-runs.json + qa-sessions.json → SQL). When true, every QA
@@ -3605,7 +3588,7 @@ const ENGINE_DEFAULTS = {
3605
3588
  agentMemorySummaryEnabled: false, // opt-in: when true, eviction batches go through an LLM-compressed summary before being dropped. Default off to mirror the conservative gating on the existing reconcile pass (LLM cost + test stability). Operators flip via engine.agentMemorySummaryEnabled.
3606
3589
  agentMemorySummaryThreshold: 30, // batch window: when summary is enabled and a prune evicts entries, fold at least this many oldest sections into one summary. Means "summary every ~30 entries" in steady state (the original PRD intent).
3607
3590
  agentMemorySummaryDays: 30, // age trigger: when the oldest section is older than this and >= agentMemorySummaryThreshold entries exist, summarize the oldest window even if the file is under the entry cap.
3608
- memoryRetrievalShadowMode: true, // compute + measure FTS5 retrieval but preserve legacy prompt injection until explicitly disabled
3591
+ memoryRetrievalShadowMode: false, // inject bounded FTS5-selected memory by default; true computes telemetry while preserving legacy prompt injection
3609
3592
  memoryRetrievalTopK: 8, // maximum structured memory records injected into one dispatch
3610
3593
  memoryRetrievalMaxBytes: 8 * 1024, // hard byte budget for the Relevant Memory prompt appendix
3611
3594
  memoryRetrievalCandidateLimit: 50, // bounded FTS5 candidate pool before deterministic re-ranking
@@ -3863,7 +3846,7 @@ function resolveCcUseWorkerPool(engine) {
3863
3846
  if (engine && (engine.ccUseWorkerPool === true || engine.ccUseWorkerPool === false)) {
3864
3847
  return engine.ccUseWorkerPool;
3865
3848
  }
3866
- return true; // CC runtime is copilot — default ON (cold-start savings)
3849
+ return ENGINE_DEFAULTS.ccUseWorkerPool;
3867
3850
  }
3868
3851
 
3869
3852
  /**
@@ -3988,39 +3971,6 @@ function resolveAgentBareMode(agent, engine) {
3988
3971
  return false;
3989
3972
  }
3990
3973
 
3991
- // P-mcp-storm — Resolve the list of MCP server names to disable for spawned
3992
- // Copilot agents. The engine FILTERS these out of the agent's seeded
3993
- // COPILOT_HOME mcp-config (engine.js `_applyAgentCopilotHome`) — a server absent
3994
- // from the config can't be spawned, which is the only effective disable
3995
- // (`--disable-mcp-server` does not stop the process). Chain: per-agent
3996
- // `agent.copilotAgentDisabledMcpServers` → `engine.copilotAgentDisabledMcpServers`
3997
- // → `[]` (inherit all). Tolerant of either an array OR a comma/whitespace-
3998
- // separated string (Settings UI / `MINIONS_*` env deliver strings). Always
3999
- // returns a deduped array of trimmed, non-empty strings. See ENGINE_DEFAULTS
4000
- // for the full rationale.
4001
- function _normalizeMcpServerList(v) {
4002
- if (v == null) return null;
4003
- const raw = Array.isArray(v) ? v : String(v).split(/[\s,]+/);
4004
- const out = [];
4005
- const seen = new Set();
4006
- for (const item of raw) {
4007
- const name = String(item == null ? '' : item).trim();
4008
- if (!name || seen.has(name)) continue;
4009
- seen.add(name);
4010
- out.push(name);
4011
- }
4012
- return out;
4013
- }
4014
-
4015
- function resolveCopilotAgentDisabledMcpServers(agent, engine) {
4016
- const a = agent ? _normalizeMcpServerList(agent.copilotAgentDisabledMcpServers) : null;
4017
- if (a) return a;
4018
- const e = engine ? _normalizeMcpServerList(engine.copilotAgentDisabledMcpServers) : null;
4019
- if (e) return e;
4020
- return [];
4021
- }
4022
-
4023
-
4024
3974
  // ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
4025
3975
  //
4026
3976
  // Pre-P-3b8e5f1d, `engine.ccModel` was the single fleet-wide model knob (it
@@ -6051,91 +6001,6 @@ function resolveAgentTempBaseDir(project, engine, minionsDir) {
6051
6001
  return path.join(anchor, '.agent-temp');
6052
6002
  }
6053
6003
 
6054
- /**
6055
- * Resolve the stable, repo-external COPILOT_HOME used to isolate spawned
6056
- * Copilot agents from the operator's interactive `~/.copilot` MCP servers
6057
- * (W-copilot-auth-popup-storm / PR #321 — "give copilot agents an MCP-free
6058
- * COPILOT_HOME (kill auth-popup storm)"). Copilot spawns and authenticates
6059
- * EVERY server in `$COPILOT_HOME/mcp-config.json` on every process start;
6060
- * `--disable-mcp-server <name>` only hides that server's tools from the model,
6061
- * it does NOT stop the server process from launching (verified live: agents
6062
- * spawned azmcp.exe/workiq.exe despite those servers being in
6063
- * `copilotAgentDisabledMcpServers`). The only effective lever is to point
6064
- * agents at a COPILOT_HOME whose mcp-config has NO disabled-list servers — see
6065
- * `ensureAgentCopilotHome` below, which the engine seeds at boot and re-stamps
6066
- * per dispatch (`_applyAgentCopilotHome` in engine.js).
6067
- *
6068
- * `COPILOT_HOME` overrides `$HOME/.copilot`; copilot auth is unaffected (it
6069
- * uses GH_TOKEN / the OS credential store, not files under the home), and the
6070
- * operator's real `~/.copilot` — i.e. their interactive copilot — is
6071
- * completely untouched.
6072
- *
6073
- * Placed OUTSIDE the repo so concurrent git branch switches in the working
6074
- * tree can never delete or dirty it, on the MINIONS_DIR volume (copilot's
6075
- * session-store can grow to GBs, so keep it off a possibly-full system
6076
- * drive). Stable path so copilot session resume (`--resume`) stays
6077
- * consistent across spawns. Falls back to the user home when no minionsDir
6078
- * is given.
6079
- *
6080
- * Base selection is cross-platform (W-copilot-home-posix-root): on Windows
6081
- * the MINIONS_DIR drive root (`C:\`, `D:\`, or a UNC share root) is
6082
- * user-writable, so we anchor there. On POSIX the filesystem root (`/`) is
6083
- * NOT user-writable — a home placed there can never be created (`mkdir`
6084
- * EACCES), `ensureAgentCopilotHome` fail-opens and returns the uncreatable
6085
- * path anyway, and every spawned `copilot --acp` / copilot agent then
6086
- * inherits a `COPILOT_HOME` it can't use and exits code 1 silently. So when
6087
- * the drive root is the bare POSIX root we fall back to the user home —
6088
- * repo-external, writable, and on the user's own volume.
6089
- */
6090
- function resolveAgentCopilotHome(minionsDir) {
6091
- let base;
6092
- if (minionsDir) {
6093
- const root = path.parse(path.resolve(String(minionsDir))).root;
6094
- // The POSIX filesystem root ('/') is not user-writable; Windows drive/UNC
6095
- // roots are. Anchor at the user home in the unwritable-root case.
6096
- base = (root === '/' || root === path.sep) ? os.homedir() : root;
6097
- } else {
6098
- base = os.homedir();
6099
- }
6100
- return path.join(base, '.minions-agent-copilot-home');
6101
- }
6102
-
6103
- // Seed the agent COPILOT_HOME's mcp-config (copy of `~/.copilot/mcp-config.json`
6104
- // MINUS `engine.copilotAgentDisabledMcpServers`) and return the home path. This
6105
- // is the source of truth for "what MCP servers may an agent-dispatch copilot
6106
- // load." It is applied to the engine-process copilot spawn paths ONLY — agent
6107
- // dispatches (`_applyAgentCopilotHome`) and the engine process's own internal
6108
- // `llm.callLLM` calls — by setting `process.env.COPILOT_HOME` at engine boot
6109
- // and re-stamping it per dispatch, so those copilot CHILDREN inherit the
6110
- // filtered config. copilot SPAWNS every server in its config and
6111
- // authenticates it (`--disable-mcp-server` only hides tools), so any
6112
- // auth-requiring server pops a Microsoft window per spawn — filtering the
6113
- // config is the only effective control. Idempotent: writes only when the
6114
- // content changes (safe under concurrent callers). The operator's real
6115
- // `~/.copilot` (interactive copilot, and now also Command Center / doc-chat /
6116
- // the CC worker pool, per W-mre75l6x00024f49) is never touched. Fail-open.
6117
- function ensureAgentCopilotHome(minionsDir, engine) {
6118
- const home = resolveAgentCopilotHome(minionsDir);
6119
- try {
6120
- fs.mkdirSync(home, { recursive: true });
6121
- const disabled = new Set(resolveCopilotAgentDisabledMcpServers(null, engine));
6122
- let servers = {};
6123
- try {
6124
- const userCfgPath = path.join(os.homedir(), '.copilot', 'mcp-config.json');
6125
- const userCfg = JSON.parse(fs.readFileSync(userCfgPath, 'utf8').replace(/^/, ''));
6126
- for (const [name, def] of Object.entries(userCfg.mcpServers || {})) {
6127
- if (!disabled.has(name)) servers[name] = def;
6128
- }
6129
- } catch { servers = {}; /* no/unreadable user config → no MCPs */ }
6130
- const desired = JSON.stringify({ mcpServers: servers }, null, 2);
6131
- const cfgFile = path.join(home, 'mcp-config.json');
6132
- let current = null;
6133
- try { current = fs.readFileSync(cfgFile, 'utf8'); } catch {}
6134
- if (current !== desired) fs.writeFileSync(cfgFile, desired);
6135
- } catch { /* fail-open: leave whatever home/config exists */ }
6136
- return home;
6137
- }
6138
-
6139
6004
  // ── Spawn cwd vs worktree placement (W-mp73x32w000l143d) ──────────────────────
6140
6005
  // Work types that don't need a git worktree — they read repo state but don't
6141
6006
  // produce code changes. Centralized here so engine.js spawnAgent and any
@@ -9056,7 +8921,6 @@ module.exports = {
9056
8921
  resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
9057
8922
  resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentUseWorkerPool, resolveAgentAcpPoolSize, resolveAgentModel, resolveCcModel,
9058
8923
  resolveAgentMaxBudget, resolveAgentBareMode,
9059
- resolveCopilotAgentDisabledMcpServers,
9060
8924
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
9061
8925
  runtimeConfigWarnings,
9062
8926
  projectWorkSourceWarnings,
@@ -9162,9 +9026,7 @@ module.exports = {
9162
9026
  parseWorktreePorcelain,
9163
9027
  assertWorktreeOutsideProject,
9164
9028
  resolveProjectRootDir,
9165
- resolveAgentCopilotHome,
9166
9029
  resolveAgentTempBaseDir,
9167
- ensureAgentCopilotHome,
9168
9030
  resolveSpawnPaths,
9169
9031
  validateWorkItemWorkdir,
9170
9032
  applyWorkdir,
package/engine.js CHANGED
@@ -2100,32 +2100,6 @@ async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts
2100
2100
  }
2101
2101
  }
2102
2102
 
2103
- // Seed a spawned agent's COPILOT_HOME mcp-config as a copy of the operator's
2104
- // `~/.copilot/mcp-config.json` MINUS the servers in
2105
- // `engine.copilotAgentDisabledMcpServers`, then point COPILOT_HOME at it.
2106
- //
2107
- // Why config-filtering and not `--disable-mcp-server`: copilot SPAWNS every
2108
- // server in its config on startup and authenticates it; `--disable-mcp-server
2109
- // <name>` only hides that server's TOOLS from the model, it does NOT stop the
2110
- // server process (PR #321 — verified live: agents spawned azmcp.exe/workiq.exe
2111
- // despite those servers being in `copilotAgentDisabledMcpServers`).
2112
- //
2113
- // Default disabled list `[]` => agents inherit ALL of the operator's MCP servers
2114
- // (non-breaking; matches interactive copilot). Add a name to disable it for
2115
- // agents. COPILOT_HOME is copilot-specific; claude/codex ignore it, so this is
2116
- // set unconditionally (no runtime.name branching, per the adapter contract).
2117
- // Copilot auth is unaffected (GH_TOKEN / OS credential store); the operator's
2118
- // real ~/.copilot is untouched. Best-effort/fail-open: any failure leaves the
2119
- // inherited COPILOT_HOME so a hiccup never blocks a dispatch.
2120
- function _applyAgentCopilotHome(childEnv) {
2121
- // Re-seed (picks up Settings changes to the disabled list) + stamp the env.
2122
- // process.env.COPILOT_HOME is also set at engine boot (see below) so the
2123
- // child would inherit it anyway; this is belt-and-suspenders + keeps the
2124
- // seed fresh per dispatch. See shared.ensureAgentCopilotHome.
2125
- try { childEnv.COPILOT_HOME = shared.ensureAgentCopilotHome(MINIONS_DIR, getConfig()?.engine); }
2126
- catch { /* leave inherited COPILOT_HOME untouched */ }
2127
- }
2128
-
2129
2103
  // Point a spawned agent's TEMP/TMP/TMPDIR at a scratch dir on the worktree
2130
2104
  // volume instead of the (often full) system drive — see
2131
2105
  // shared.resolveAgentTempBaseDir. copilot/node/git + JVM-based MCP servers write
@@ -4598,16 +4572,6 @@ async function spawnAgent(dispatchItem, config) {
4598
4572
  // random localhost port (blank window). Belt-and-suspenders with dashboard.js's
4599
4573
  // stdout.isTTY gate.
4600
4574
  childEnv.MINIONS_NO_AUTO_OPEN = '1';
4601
- // MCP-free copilot config so agents never pop a per-spawn Microsoft auth
4602
- // window — see _applyAgentCopilotHome / shared.resolveAgentCopilotHome.
4603
- // P-1d8f0b93 — SKIPPED for pooled dispatches: the pooled ACP worker is a
4604
- // long-lived process shared across many dispatches, so it always uses the
4605
- // operator's REAL COPILOT_HOME (this is the isolation-removal half of the
4606
- // pooling feature — MCP-auth-popup risk becomes bounded by pool size
4607
- // instead of eliminated per-dispatch; documented tradeoff in the plan).
4608
- if (!useAgentPool) {
4609
- _applyAgentCopilotHome(childEnv);
4610
- }
4611
4575
  // Keep agent scratch off a (possibly full) system drive — anchors TEMP/TMP to
4612
4576
  // the worktree volume. See _applyAgentTempEnv / shared.resolveAgentTempBaseDir.
4613
4577
  _applyAgentTempEnv(childEnv, project);
@@ -5059,8 +5023,6 @@ async function spawnAgent(dispatchItem, config) {
5059
5023
  // W-mqef-dashboard-tty — same browser-popup suppression on steering resume
5060
5024
  // (see the initial spawn site). Agents must never auto-open a browser.
5061
5025
  childEnv.MINIONS_NO_AUTO_OPEN = '1';
5062
- // Same MCP-free copilot home on steering resume (see the initial spawn site).
5063
- _applyAgentCopilotHome(childEnv);
5064
5026
  // Same agent-scratch redirect on steering resume.
5065
5027
  _applyAgentTempEnv(childEnv, project);
5066
5028
  if (getRepoHost(project) === 'ado') {
@@ -11685,17 +11647,6 @@ function ensureGitHooksInstalled() {
11685
11647
 
11686
11648
  if (require.main === module) {
11687
11649
  try { ensureGitHooksInstalled(); } catch { /* best-effort */ }
11688
- // Every copilot this process spawns — agent dispatches AND internal
11689
- // `llm.callLLM` calls (consolidation, classification, meetings, …) — inherits
11690
- // this process's COPILOT_HOME, so point it at the seeded, MCP-filtered home.
11691
- // Without this, those `direct:true` copilot calls load the operator's full
11692
- // `~/.copilot` stack and pop a Microsoft auth window per call. This isolation
11693
- // is engine-process-only: the dashboard process deliberately does NOT set
11694
- // COPILOT_HOME at its own boot, so Command Center / doc-chat / the CC worker
11695
- // pool inherit the operator's real `~/.copilot` home instead (reverted in
11696
- // W-mre75l6x00024f49; see dashboard.js boot for rationale).
11697
- // See shared.ensureAgentCopilotHome. Fail-open.
11698
- try { process.env.COPILOT_HOME = shared.ensureAgentCopilotHome(MINIONS_DIR, getConfig()?.engine); } catch {}
11699
11650
  const { handleCommand } = require('./engine/cli');
11700
11651
  const [cmd, ...args] = process.argv.slice(2);
11701
11652
  handleCommand(cmd, args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2374",
3
+ "version": "0.1.2376",
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"