@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.
- package/dashboard/js/settings.js +3 -8
- package/dashboard.js +0 -23
- package/docs/pr-review-fix-loop.md +4 -2
- package/docs/shared-lifecycle-module-map.md +0 -2
- package/docs/team-memory.md +4 -4
- package/engine/features.js +2 -2
- package/engine/github.js +97 -73
- package/engine/inbox-store.js +19 -0
- package/engine/shared.js +3 -141
- package/engine.js +0 -49
- package/package.json +1 -1
package/dashboard/js/settings.js
CHANGED
|
@@ -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
|
|
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()` |
|
|
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 |
|
package/docs/team-memory.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
23
|
+
Structured retrieval is controlled by the default-on `memoryRetrieval` feature flag:
|
|
24
24
|
|
|
25
25
|
- **Flag off:** legacy prompt behavior.
|
|
26
|
-
- **Flag on +
|
|
27
|
-
- **Flag on +
|
|
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
|
|
package/engine/features.js
CHANGED
|
@@ -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
|
|
100
|
-
default:
|
|
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(
|
|
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
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
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 =>
|
|
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
|
package/engine/inbox-store.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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.
|
|
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"
|