@yemi33/minions 0.1.2177 → 0.1.2179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/minions.js +24 -11
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +262 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +9 -1
- package/dashboard/js/render-dispatch.js +92 -0
- package/dashboard/js/render-other.js +1 -1
- package/dashboard/js/render-plans.js +82 -13
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/js/settings.js +100 -11
- package/dashboard/layout.html +6 -0
- package/dashboard/pages/engine-memory-panel.html +49 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard/styles.css +39 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +469 -21
- package/docs/README.md +8 -1
- package/docs/auto-discovery.md +40 -0
- package/docs/branch-derivation.md +13 -1
- package/docs/cross-repo-plans.md +292 -0
- package/docs/deprecated.json +4 -4
- package/docs/pr-auto-fix-dispatch.md +64 -0
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/watches.md +1 -0
- package/engine/ado.js +1 -10
- package/engine/diagnostics-memory.js +190 -0
- package/engine/dispatch.js +53 -0
- package/engine/lifecycle.js +155 -191
- package/engine/meeting.js +30 -0
- package/engine/playbook.js +15 -0
- package/engine/queries.js +165 -5
- package/engine/runtimes/copilot.js +19 -0
- package/engine/shared.js +303 -3
- package/engine/watchdog.js +6 -0
- package/engine.js +576 -113
- package/package.json +2 -2
- package/playbooks/plan-to-prd.md +25 -2
- package/playbooks/plan.md +4 -2
package/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const crypto = require('crypto');
|
|
|
32
32
|
const shared = require('./engine/shared');
|
|
33
33
|
const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
|
|
34
34
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, AGENT_STATUS,
|
|
35
|
-
FAILURE_CLASS } = shared;
|
|
35
|
+
FAILURE_CLASS, resolvePollFlag } = shared;
|
|
36
36
|
const { resolveRuntime } = require('./engine/runtimes');
|
|
37
37
|
const { assertStaleHeadOk } = require('./engine/spawn-agent');
|
|
38
38
|
const adoGitAuth = require('./engine/ado-git-auth');
|
|
@@ -168,7 +168,12 @@ const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconci
|
|
|
168
168
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
169
169
|
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
|
|
170
170
|
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath,
|
|
171
|
-
mergeArtifactNotes, promoteCompletionArtifacts } = require('./engine/lifecycle');
|
|
171
|
+
mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs } = require('./engine/lifecycle');
|
|
172
|
+
|
|
173
|
+
// ─── Diagnostics: memory + event-loop + GC sampler (P-a1b2c3d4 / P-b2c3d4e5) ─
|
|
174
|
+
|
|
175
|
+
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
176
|
+
const DIAGNOSTICS_MEMORY_PATH = path.join(ENGINE_DIR, 'diagnostics-memory.json');
|
|
172
177
|
|
|
173
178
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
174
179
|
|
|
@@ -617,7 +622,11 @@ function promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutpu
|
|
|
617
622
|
|
|
618
623
|
// Resolve dependency plan item IDs to their PR branches
|
|
619
624
|
function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
620
|
-
|
|
625
|
+
// P-faea3206: each entry now carries projectName + projectRoot + isCrossRepo
|
|
626
|
+
// so spawnAgent's dep-fetch loop can route same-project deps through the
|
|
627
|
+
// existing fetch+merge path and cross-project deps through the advisory
|
|
628
|
+
// transfer-fetch path (no auto-merge, no escalation).
|
|
629
|
+
const results = []; // [{ branch, prId, projectName, projectRoot, isCrossRepo }]
|
|
621
630
|
if (!depIds?.length) return results;
|
|
622
631
|
|
|
623
632
|
const projects = shared.getProjects(config);
|
|
@@ -626,6 +635,8 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
626
635
|
const allItems = queries.getWorkItems(config);
|
|
627
636
|
const depWorkItems = allItems.filter(wi => depIds.includes(wi.id));
|
|
628
637
|
|
|
638
|
+
const currentProjectName = project?.name || null;
|
|
639
|
+
|
|
629
640
|
// Find PR branches for each dependency work item
|
|
630
641
|
for (const p of projects) {
|
|
631
642
|
const prPath = shared.projectPrPath(p);
|
|
@@ -636,7 +647,16 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
636
647
|
depWorkItems.find(w => w.id === id)
|
|
637
648
|
);
|
|
638
649
|
if (linked && !results.find(r => r.branch === pr.branch)) {
|
|
639
|
-
|
|
650
|
+
let depRoot = null;
|
|
651
|
+
try { depRoot = shared.resolveProjectRootDir(p.localPath, MINIONS_DIR); }
|
|
652
|
+
catch { depRoot = null; /* drive-root or missing localPath — caller falls back gracefully */ }
|
|
653
|
+
results.push({
|
|
654
|
+
branch: pr.branch,
|
|
655
|
+
prId: pr.id,
|
|
656
|
+
projectName: p.name || null,
|
|
657
|
+
projectRoot: depRoot,
|
|
658
|
+
isCrossRepo: !!currentProjectName && p.name !== currentProjectName,
|
|
659
|
+
});
|
|
640
660
|
}
|
|
641
661
|
}
|
|
642
662
|
}
|
|
@@ -644,6 +664,48 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
644
664
|
return results;
|
|
645
665
|
}
|
|
646
666
|
|
|
667
|
+
// P-faea3206: render the "## Cross-repo dependencies" prompt section that
|
|
668
|
+
// gets appended to the prompt file when a dispatch has at least one
|
|
669
|
+
// cross-repo (advisory) dep. The section enumerates each dep's repo, branch,
|
|
670
|
+
// short SHA, and the changed-file list, plus the local ref
|
|
671
|
+
// (`refs/cross-repo-deps/<branch>`) the agent can `git show` / `git log` to
|
|
672
|
+
// inspect the contract without merging unrelated repo content.
|
|
673
|
+
//
|
|
674
|
+
// Returns "" when `deps` is empty/missing so callers can unconditionally
|
|
675
|
+
// concat the result without a separate guard.
|
|
676
|
+
function buildCrossRepoDepsSection(deps) {
|
|
677
|
+
if (!Array.isArray(deps) || deps.length === 0) return '';
|
|
678
|
+
const lines = [
|
|
679
|
+
'',
|
|
680
|
+
'## Cross-repo dependencies',
|
|
681
|
+
'',
|
|
682
|
+
'The work items below are dependencies in OTHER repos. They are surfaced here as',
|
|
683
|
+
'*advisory* context only — the engine intentionally does NOT merge cross-repo dep',
|
|
684
|
+
'branches into your worktree (that would commingle unrelated repo content). Use the',
|
|
685
|
+
'local refs below to read the contract (`git show <ref>`, `git log <ref> -- <files>`)',
|
|
686
|
+
'and update your code in this repo to honor it.',
|
|
687
|
+
'',
|
|
688
|
+
];
|
|
689
|
+
for (const dep of deps) {
|
|
690
|
+
const branch = dep?.branch || '(unknown-branch)';
|
|
691
|
+
const repo = dep?.projectName || '(unknown-repo)';
|
|
692
|
+
const sha = typeof dep?.sha === 'string' && dep.sha ? dep.sha.slice(0, 12) : '';
|
|
693
|
+
const files = Array.isArray(dep?.files) ? dep.files.filter(Boolean) : [];
|
|
694
|
+
lines.push(`### ${repo} — \`${branch}\``);
|
|
695
|
+
if (sha) lines.push(`- Tip SHA: \`${sha}\``);
|
|
696
|
+
lines.push(`- Local ref: \`refs/cross-repo-deps/${branch}\``);
|
|
697
|
+
if (files.length > 0) {
|
|
698
|
+
lines.push('- Changed files:');
|
|
699
|
+
for (const f of files.slice(0, 50)) lines.push(` - \`${f}\``);
|
|
700
|
+
if (files.length > 50) lines.push(` - …and ${files.length - 50} more`);
|
|
701
|
+
} else {
|
|
702
|
+
lines.push('- Changed files: (not computed — inspect via `git log --stat`)');
|
|
703
|
+
}
|
|
704
|
+
lines.push('');
|
|
705
|
+
}
|
|
706
|
+
return lines.join('\n');
|
|
707
|
+
}
|
|
708
|
+
|
|
647
709
|
/**
|
|
648
710
|
* Sync an existing worktree from origin on reuse: probe with
|
|
649
711
|
* `git ls-remote --exit-code --heads origin <branch>` first so that locally
|
|
@@ -1572,6 +1634,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1572
1634
|
// milliseconds. Keys are stamped at phase boundaries below.
|
|
1573
1635
|
const _phaseT = { start: Date.now() };
|
|
1574
1636
|
let _depCountForLog = 0;
|
|
1637
|
+
// P-faea3206: collected info for the prompt-file "## Cross-repo dependencies"
|
|
1638
|
+
// section. Populated during the dep-fetch phase below; consumed once the
|
|
1639
|
+
// worktree is ready and the initial prompt has been written (alongside the
|
|
1640
|
+
// dirty-files inject). Stays an empty array on single-repo / no-dep dispatches.
|
|
1641
|
+
let _crossRepoDepsInfo = [];
|
|
1575
1642
|
|
|
1576
1643
|
updateAgentStatus(id, AGENT_STATUS.SPAWNING, `Preparing ${type} task for ${agentId}`);
|
|
1577
1644
|
|
|
@@ -2438,7 +2505,26 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2438
2505
|
if (depIds.length > 0) {
|
|
2439
2506
|
_phaseT.depFetchStart = Date.now();
|
|
2440
2507
|
try {
|
|
2441
|
-
const
|
|
2508
|
+
const depBranchesAll = resolveDependencyBranches(depIds, meta?.item?.sourcePlan, project, config);
|
|
2509
|
+
// P-faea3206: partition deps into same-project (fetch + merge as
|
|
2510
|
+
// today) and cross-project (fetch into the dep's project rootDir,
|
|
2511
|
+
// transfer-fetch as refs/cross-repo-deps/<branch>, NEVER auto-merge,
|
|
2512
|
+
// NEVER escalate). Cross-repo deps without a resolvable projectRoot
|
|
2513
|
+
// (missing localPath on dep project) are dropped with a warn — the
|
|
2514
|
+
// engine can't honor them safely.
|
|
2515
|
+
const crossRepoDepsRaw = depBranchesAll.filter(d => d.isCrossRepo);
|
|
2516
|
+
const crossRepoDepsBranches = [];
|
|
2517
|
+
for (const dep of crossRepoDepsRaw) {
|
|
2518
|
+
if (!dep.projectRoot) {
|
|
2519
|
+
log('warn', `Skipping cross-repo dep ${dep.branch} (${dep.prId}) — dep project "${dep.projectName}" has no resolvable rootDir`);
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
crossRepoDepsBranches.push(dep);
|
|
2523
|
+
}
|
|
2524
|
+
// The historical loop below operates ONLY on same-project deps.
|
|
2525
|
+
// Cross-repo deps are handled in their own block immediately after
|
|
2526
|
+
// the per-branch fetch loop.
|
|
2527
|
+
const depBranches = depBranchesAll.filter(d => !d.isCrossRepo);
|
|
2442
2528
|
let depMergeFailed = false;
|
|
2443
2529
|
let depConflictBranch = null; // track which dep branch caused the conflict
|
|
2444
2530
|
let depConflictFiles = []; // conflicting file names parsed from git output
|
|
@@ -2542,6 +2628,69 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2542
2628
|
depMergeFailed = true;
|
|
2543
2629
|
}
|
|
2544
2630
|
}
|
|
2631
|
+
// P-faea3206: Cross-repo dep fetch + transfer-fetch into the
|
|
2632
|
+
// current worktree. These deps are ADVISORY — we never auto-merge
|
|
2633
|
+
// them (cross-repo merges would commingle unrelated repo content),
|
|
2634
|
+
// never trigger the local-only push recovery, and never escalate
|
|
2635
|
+
// via depMergeFailed (the dispatch proceeds regardless of cross-
|
|
2636
|
+
// repo dep fetch outcome). The local ref `refs/cross-repo-deps/
|
|
2637
|
+
// <branch>` lets the agent inspect the contract via git show /
|
|
2638
|
+
// git log without touching the working tree.
|
|
2639
|
+
for (const dep of crossRepoDepsBranches) {
|
|
2640
|
+
const depBranch = dep.branch;
|
|
2641
|
+
const depProjectRoot = dep.projectRoot;
|
|
2642
|
+
try {
|
|
2643
|
+
// 1. Fetch the dep branch into the DEP project's rootDir
|
|
2644
|
+
// (the current project's origin doesn't carry that ref).
|
|
2645
|
+
await shared.shellSafeGit(['fetch', 'origin', depBranch], { ..._gitOpts, cwd: depProjectRoot });
|
|
2646
|
+
// 2. Transfer the tip into the current worktree via a
|
|
2647
|
+
// local-path fetch — git supports filesystem-path remotes.
|
|
2648
|
+
// Stamp it under refs/cross-repo-deps/<branch> so it's
|
|
2649
|
+
// namespaced away from refs/heads / refs/remotes and
|
|
2650
|
+
// cannot accidentally be checked out / pushed.
|
|
2651
|
+
const refSpec = `${depBranch}:refs/cross-repo-deps/${depBranch}`;
|
|
2652
|
+
await shared.shellSafeGit(['fetch', depProjectRoot, refSpec], { ..._gitOpts, cwd: worktreePath });
|
|
2653
|
+
// 3. Resolve the tip SHA + a short changed-file list (vs the
|
|
2654
|
+
// dep project's main) for the prompt section. These are
|
|
2655
|
+
// best-effort — failure here downgrades to an empty list
|
|
2656
|
+
// rather than failing the whole advisory block.
|
|
2657
|
+
let depSha = '';
|
|
2658
|
+
try {
|
|
2659
|
+
depSha = gitOutputToString(await shared.shellSafeGit(
|
|
2660
|
+
['rev-parse', `refs/cross-repo-deps/${depBranch}`],
|
|
2661
|
+
{ ..._gitOpts, cwd: worktreePath }
|
|
2662
|
+
)).trim();
|
|
2663
|
+
} catch (_) { /* best-effort */ }
|
|
2664
|
+
let depFiles = [];
|
|
2665
|
+
try {
|
|
2666
|
+
const depMainRef = sanitizeBranch(shared.resolveMainBranch(depProjectRoot, null));
|
|
2667
|
+
const diffOut = gitOutputToString(await shared.shellSafeGit(
|
|
2668
|
+
['diff', '--name-only', `origin/${depMainRef}...${depBranch}`],
|
|
2669
|
+
{ ..._gitOpts, cwd: depProjectRoot }
|
|
2670
|
+
));
|
|
2671
|
+
depFiles = diffOut.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
2672
|
+
} catch (_) { /* best-effort */ }
|
|
2673
|
+
_crossRepoDepsInfo.push({
|
|
2674
|
+
branch: depBranch,
|
|
2675
|
+
projectName: dep.projectName,
|
|
2676
|
+
sha: depSha,
|
|
2677
|
+
files: depFiles,
|
|
2678
|
+
});
|
|
2679
|
+
log('info', `Cross-repo dep ${dep.projectName}:${depBranch} transferred as refs/cross-repo-deps/${depBranch}${depFiles.length ? ` (${depFiles.length} changed file(s))` : ''}`);
|
|
2680
|
+
} catch (crossErr) {
|
|
2681
|
+
// ADVISORY: log and move on. No depMergeFailed flip, no
|
|
2682
|
+
// recovery push, no failure-class escalation. Surface the
|
|
2683
|
+
// miss to the agent prompt anyway so it knows the dep was
|
|
2684
|
+
// attempted (with files: []) — better than silent omission.
|
|
2685
|
+
log('warn', `Cross-repo dep ${dep.projectName}:${depBranch} fetch failed (advisory — dispatch proceeds): ${crossErr.message}`);
|
|
2686
|
+
_crossRepoDepsInfo.push({
|
|
2687
|
+
branch: depBranch,
|
|
2688
|
+
projectName: dep.projectName,
|
|
2689
|
+
sha: '',
|
|
2690
|
+
files: [],
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2545
2694
|
_phaseT.depFetchEnd = Date.now();
|
|
2546
2695
|
_phaseT.depPreflightStart = _phaseT.depFetchEnd;
|
|
2547
2696
|
// Merge successfully-fetched + recovered (local-only pushed) branches sequentially
|
|
@@ -2864,6 +3013,20 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2864
3013
|
_phaseT.dirtyProbeEnd = Date.now();
|
|
2865
3014
|
}
|
|
2866
3015
|
|
|
3016
|
+
// P-faea3206: Append a "## Cross-repo dependencies" section to the prompt
|
|
3017
|
+
// file once the dep-fetch phase has populated _crossRepoDepsInfo. Lands
|
|
3018
|
+
// AFTER the shared-branch prompt refresh and the dirty-files inject so the
|
|
3019
|
+
// section is preserved regardless of which earlier write path ran. The
|
|
3020
|
+
// section is advisory: cross-repo deps were fetched into refs/cross-repo-
|
|
3021
|
+
// deps/<branch> but NOT merged into the working tree.
|
|
3022
|
+
if (_crossRepoDepsInfo.length > 0) {
|
|
3023
|
+
const crossSection = buildCrossRepoDepsSection(_crossRepoDepsInfo);
|
|
3024
|
+
if (crossSection) {
|
|
3025
|
+
try { fs.appendFileSync(promptPath, crossSection); } catch (e) { log('warn', `cross-repo deps inject: ${e.message}`); }
|
|
3026
|
+
log('info', `Injected ${_crossRepoDepsInfo.length} cross-repo dep(s) into prompt for ${id}`);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2867
3030
|
// Safety check: warn if a write-capable task is running in the main repo without a worktree
|
|
2868
3031
|
if (cwd === rootDir && ['implement', 'implement:large', 'fix', 'test', 'verify', 'plan-to-prd'].includes(type)) {
|
|
2869
3032
|
log('warn', `Agent ${agentId} running ${type} task in main repo (no worktree) for ${id} — changes may land on master directly`);
|
|
@@ -2917,6 +3080,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2917
3080
|
const resolvedModel = runtime.resolveModel(shared.resolveAgentModel(agentConfig, engineConfig));
|
|
2918
3081
|
const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
|
|
2919
3082
|
const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
|
|
3083
|
+
// P-mcp-storm (opg#134 backport) — user-scope MCP servers to keep out of
|
|
3084
|
+
// this agent dispatch. Copilot loads ~/.copilot/mcp-config.json
|
|
3085
|
+
// unconditionally; the adapter re-emits these as --disable-mcp-server <name>.
|
|
3086
|
+
// Other runtimes ignore the opt (their buildSpawnFlags don't read it), so no
|
|
3087
|
+
// runtime.name branch here.
|
|
3088
|
+
const resolvedDisabledMcpServers = shared.resolveCopilotAgentDisabledMcpServers(agentConfig, engineConfig);
|
|
2920
3089
|
|
|
2921
3090
|
// W-mpg6isvy000xca4d — On retry after FAILURE_CLASS.MODEL_UNAVAILABLE, swap
|
|
2922
3091
|
// to the runtime-appropriate fallback model. Two paths gated on
|
|
@@ -2971,6 +3140,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2971
3140
|
disableBuiltinMcps: engineConfig.copilotDisableBuiltinMcps,
|
|
2972
3141
|
suppressAgentsMd: engineConfig.copilotSuppressAgentsMd,
|
|
2973
3142
|
reasoningSummaries: engineConfig.copilotReasoningSummaries,
|
|
3143
|
+
disabledMcpServers: resolvedDisabledMcpServers,
|
|
2974
3144
|
});
|
|
2975
3145
|
|
|
2976
3146
|
// MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
|
|
@@ -3365,6 +3535,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3365
3535
|
disableBuiltinMcps: engineConfig?.copilotDisableBuiltinMcps,
|
|
3366
3536
|
suppressAgentsMd: engineConfig?.copilotSuppressAgentsMd,
|
|
3367
3537
|
reasoningSummaries: engineConfig?.copilotReasoningSummaries,
|
|
3538
|
+
disabledMcpServers: resolvedDisabledMcpServers,
|
|
3368
3539
|
});
|
|
3369
3540
|
if (!resumeArgs.includes('--resume')) {
|
|
3370
3541
|
log('warn', `Steering: runtime ${runtime.name} did not accept session resume — skipping for ${agentId}`);
|
|
@@ -5163,21 +5334,43 @@ function materializePlansAsWorkItems(config) {
|
|
|
5163
5334
|
if (totalCreated > 0) {
|
|
5164
5335
|
log('info', `Plan discovery: ${totalCreated} total item(s) from ${file} across ${itemsByProject.size} project(s)`);
|
|
5165
5336
|
|
|
5166
|
-
// Pre-create shared feature branch
|
|
5337
|
+
// Pre-create shared feature branch in every touched project (P-1c0f5e84).
|
|
5338
|
+
// Cross-repo plans materialize work items into multiple projects; each
|
|
5339
|
+
// one needs the shared branch created off its own main and pushed to
|
|
5340
|
+
// its own origin so the dispatcher can later `git worktree add ... origin/<branch>`
|
|
5341
|
+
// per project. Idempotent — `git rev-parse --verify` skips the create
|
|
5342
|
+
// when the branch already exists locally, and `git push -u origin` is
|
|
5343
|
+
// a no-op when the remote already has the same tip.
|
|
5167
5344
|
if (plan.branch_strategy === 'shared-branch' && plan.feature_branch) {
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5345
|
+
const branch = sanitizeBranch(plan.feature_branch);
|
|
5346
|
+
for (const [projName, { project: itProject }] of itemsByProject) {
|
|
5347
|
+
// Skip the _central bucket — those items have no project root.
|
|
5348
|
+
if (!itProject?.localPath) continue;
|
|
5349
|
+
try {
|
|
5350
|
+
const root = path.resolve(itProject.localPath);
|
|
5351
|
+
const mainBranch = shared.resolveMainBranch(root, itProject.mainBranch);
|
|
5352
|
+
// Idempotency guard: rev-parse --verify exits 0 when the branch
|
|
5353
|
+
// already exists locally. Only attempt the branch-create when
|
|
5354
|
+
// it returns a nonzero exit (caught below).
|
|
5355
|
+
let branchExists = false;
|
|
5356
|
+
try {
|
|
5357
|
+
shared.shellSafeGitSync(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: root });
|
|
5358
|
+
branchExists = true;
|
|
5359
|
+
} catch { /* branch does not exist — fall through to create */ }
|
|
5360
|
+
if (!branchExists) {
|
|
5361
|
+
try { shared.shellSafeGitSync(['branch', branch, mainBranch], { cwd: root }); }
|
|
5362
|
+
catch (e) { log('warn', `Shared branch ${branch} create in ${projName} skipped: ${e.message?.split('\n')[0]}`); }
|
|
5363
|
+
}
|
|
5364
|
+
// Always push — no-op when origin already has the tip; surfaces
|
|
5365
|
+
// the new branch when it was just created.
|
|
5366
|
+
try { shared.shellSafeGitSync(['push', '-u', 'origin', branch], { cwd: root }); }
|
|
5367
|
+
catch (e) { log('warn', `git push -u origin ${branch} (pre-create in ${projName}): ${e.message?.split('\n')[0]}`); }
|
|
5368
|
+
log('info', `Shared branch pre-created: ${branch} in ${projName} for plan ${file}`);
|
|
5369
|
+
} catch (err) {
|
|
5370
|
+
// Per-project failure isolation — one project's broken
|
|
5371
|
+
// localPath / git config must not block the others.
|
|
5372
|
+
log('warn', `Failed to pre-create shared branch ${branch} in ${projName} for ${file}: ${err.message}`);
|
|
5373
|
+
}
|
|
5181
5374
|
}
|
|
5182
5375
|
}
|
|
5183
5376
|
|
|
@@ -5415,6 +5608,44 @@ function isPrAutomationCauseHandledOrPending(project, pr, causeKey) {
|
|
|
5415
5608
|
}
|
|
5416
5609
|
|
|
5417
5610
|
|
|
5611
|
+
/**
|
|
5612
|
+
* P-a1f3c2d4 — `engine.pollingPaused` is a hard-stop kill-switch that fires
|
|
5613
|
+
* every tick when ON. Without this guard the engine would emit an info log on
|
|
5614
|
+
* every tick (~6/min) while paused, drowning the rest of the log. We log once
|
|
5615
|
+
* on the transition `unpaused → paused`, then stay quiet until the operator
|
|
5616
|
+
* flips it back off (transition `paused → unpaused` is silent — the engine's
|
|
5617
|
+
* routine "PR status poll" log entries resume on their own and visibly signal
|
|
5618
|
+
* the unpause).
|
|
5619
|
+
*/
|
|
5620
|
+
let _pollingPausedLastState = false;
|
|
5621
|
+
function _shouldLogPollingPausedOnce() {
|
|
5622
|
+
if (_pollingPausedLastState) return false;
|
|
5623
|
+
_pollingPausedLastState = true;
|
|
5624
|
+
return true;
|
|
5625
|
+
}
|
|
5626
|
+
function _resetPollingPausedLogState() {
|
|
5627
|
+
// Only flips back to "log next time we see paused=true" when we observe
|
|
5628
|
+
// paused=false. Called from the polling-gate resolution site below
|
|
5629
|
+
// whenever pollingPaused is currently false.
|
|
5630
|
+
_pollingPausedLastState = false;
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
/**
|
|
5634
|
+
* P-b2e5d8c7 — `engine.autoFixPaused` mirrors the pollingPaused log-throttle
|
|
5635
|
+
* pattern above. Same once-per-transition contract: log on unpaused → paused,
|
|
5636
|
+
* stay quiet while paused, silent on paused → unpaused (routine discovery
|
|
5637
|
+
* resumes and signals the unpause).
|
|
5638
|
+
*/
|
|
5639
|
+
let _autoFixPausedLastState = false;
|
|
5640
|
+
function _shouldLogAutoFixPausedOnce() {
|
|
5641
|
+
if (_autoFixPausedLastState) return false;
|
|
5642
|
+
_autoFixPausedLastState = true;
|
|
5643
|
+
return true;
|
|
5644
|
+
}
|
|
5645
|
+
function _resetAutoFixPausedLogState() {
|
|
5646
|
+
_autoFixPausedLastState = false;
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5418
5649
|
// Tracks per-process which silent-discovery warnings have already been logged
|
|
5419
5650
|
// so we don't spam the log every tick. Cleared on process exit (no need to
|
|
5420
5651
|
// persist — the warning is for the operator at engine startup/run time).
|
|
@@ -5467,17 +5698,31 @@ async function discoverFromPrs(config, project) {
|
|
|
5467
5698
|
const newWork = [];
|
|
5468
5699
|
|
|
5469
5700
|
const projMeta = { name: project?.name, localPath: project?.localPath };
|
|
5470
|
-
// Resolve poll-enabled per project — stale reviewStatus is untrustworthy without poller
|
|
5701
|
+
// Resolve poll-enabled per project — stale reviewStatus is untrustworthy without poller.
|
|
5702
|
+
// P-a1f3c2d4: engine.pollingPaused is a hard-stop master switch that wins over
|
|
5703
|
+
// both adoPollEnabled and ghPollEnabled. Forcing pollEnabled=false here
|
|
5704
|
+
// suppresses every per-PR auto-dispatch gate below (reviewEnabled,
|
|
5705
|
+
// autoFixHumanComments, autoFixReviewFeedback, autoFixBuilds, autoFixConflicts)
|
|
5706
|
+
// without us having to add a check at each call site.
|
|
5707
|
+
const pollingPaused = config.engine?.pollingPaused === true;
|
|
5471
5708
|
const isAdoProject = project?.repoHost !== 'github';
|
|
5472
|
-
const pollEnabled = isAdoProject
|
|
5709
|
+
const pollEnabled = !pollingPaused && (isAdoProject
|
|
5473
5710
|
? (config.engine?.adoPollEnabled ?? ENGINE_DEFAULTS.adoPollEnabled)
|
|
5474
|
-
: (config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled);
|
|
5711
|
+
: (config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled));
|
|
5475
5712
|
const evalLoopEnabled = config.engine?.evalLoop !== false;
|
|
5476
5713
|
const fixThrottled = isAdoProject ? isAdoThrottled() : isGhThrottled();
|
|
5477
5714
|
const autoReviewPrs = config.engine?.autoReviewPrs ?? ENGINE_DEFAULTS.autoReviewPrs;
|
|
5478
5715
|
const autoReReviewPrs = config.engine?.autoReReviewPrs ?? ENGINE_DEFAULTS.autoReReviewPrs;
|
|
5479
|
-
|
|
5480
|
-
|
|
5716
|
+
// P-b2e5d8c7: engine.autoFixPaused is a hard-stop master switch over every
|
|
5717
|
+
// auto-fix gate. Composing `!autoFixPaused && …` into each autoFix* gate
|
|
5718
|
+
// here makes the kill-switch single-point: every downstream
|
|
5719
|
+
// `pollEnabled && autoFix* && …` site inherits the gate without us touching
|
|
5720
|
+
// the per-cause dispatch blocks. Review / re-review dispatch (autoReviewPrs
|
|
5721
|
+
// / autoReReviewPrs) is intentionally NOT gated — operators can pause the
|
|
5722
|
+
// fix-storm during an incident while still seeing fresh review verdicts.
|
|
5723
|
+
const autoFixPaused = config.engine?.autoFixPaused === true;
|
|
5724
|
+
const autoFixReviewFeedback = !autoFixPaused && (config.engine?.autoFixReviewFeedback ?? ENGINE_DEFAULTS.autoFixReviewFeedback);
|
|
5725
|
+
const autoFixHumanComments = !autoFixPaused && (config.engine?.autoFixHumanComments ?? ENGINE_DEFAULTS.autoFixHumanComments);
|
|
5481
5726
|
|
|
5482
5727
|
// Collect active PR dispatches to prevent simultaneous review+fix on same PR
|
|
5483
5728
|
const dispatch = getDispatch();
|
|
@@ -5861,7 +6106,7 @@ async function discoverFromPrs(config, project) {
|
|
|
5861
6106
|
const gracePeriodMs = config.engine?.buildFixGracePeriod ?? ENGINE_DEFAULTS.buildFixGracePeriod;
|
|
5862
6107
|
if (Date.now() - new Date(pr._buildFixPushedAt).getTime() < gracePeriodMs) continue;
|
|
5863
6108
|
}
|
|
5864
|
-
const autoFixBuilds = config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds;
|
|
6109
|
+
const autoFixBuilds = !autoFixPaused && (config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds);
|
|
5865
6110
|
if (pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing'
|
|
5866
6111
|
&& !fixDispatched
|
|
5867
6112
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.BUILD_FAILURE)) {
|
|
@@ -6022,7 +6267,7 @@ async function discoverFromPrs(config, project) {
|
|
|
6022
6267
|
}
|
|
6023
6268
|
|
|
6024
6269
|
// PRs with merge conflicts — dispatch fix to resolve (gated by provider polling + autoFixConflicts)
|
|
6025
|
-
const autoFixConflicts = config.engine?.autoFixConflicts ?? ENGINE_DEFAULTS.autoFixConflicts;
|
|
6270
|
+
const autoFixConflicts = !autoFixPaused && (config.engine?.autoFixConflicts ?? ENGINE_DEFAULTS.autoFixConflicts);
|
|
6026
6271
|
if (pollEnabled && autoFixConflicts && pr.status === PR_STATUS.ACTIVE && pr._mergeConflict && !fixDispatched
|
|
6027
6272
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.MERGE_CONFLICT)) {
|
|
6028
6273
|
// W-mpritzcr0004afc5 (#2955): "don't fan out a parallel conflict-fix
|
|
@@ -7342,15 +7587,42 @@ function discoverCentralWorkItems(config) {
|
|
|
7342
7587
|
let planFileContent = null;
|
|
7343
7588
|
let planReadError = null;
|
|
7344
7589
|
let declaredPlanProject = '';
|
|
7590
|
+
let planTargetProjects = [];
|
|
7345
7591
|
if (workType === WORK_TYPE.PLAN_TO_PRD && item.planFile) {
|
|
7346
7592
|
const planPath = path.join(PLANS_DIR, item.planFile);
|
|
7347
7593
|
try {
|
|
7348
7594
|
planFileContent = fs.readFileSync(planPath, 'utf8');
|
|
7349
7595
|
declaredPlanProject = shared.extractPlanDeclaredProject(planFileContent);
|
|
7596
|
+
// P-9af2eb37 — also parse the cross-repo target list from the same
|
|
7597
|
+
// plan content. extractPlanTargetProjects (P-7a3f1c08) returns
|
|
7598
|
+
// [] when the plan has no <!-- minions:targetProjects=… --> marker
|
|
7599
|
+
// and no **Projects:** line, so single-project plans are a no-op.
|
|
7600
|
+
planTargetProjects = shared.extractPlanTargetProjects(planFileContent);
|
|
7350
7601
|
} catch (e) {
|
|
7351
7602
|
planReadError = e;
|
|
7352
7603
|
}
|
|
7353
7604
|
}
|
|
7605
|
+
// P-9af2eb37 — a cross-repo plan-to-prd has NO singular **Project:**
|
|
7606
|
+
// header (declaredPlanProject === '') but ≥2 entries in the parsed
|
|
7607
|
+
// target list. When that happens, attach the dispatch to the first
|
|
7608
|
+
// listed project (read-only worktree, no mutation, no collision risk)
|
|
7609
|
+
// so vars.project_path resolves and worktree-requiring carve-outs
|
|
7610
|
+
// behave. The cross-repo case is signaled to the playbook via
|
|
7611
|
+
// vars.target_projects (set further down in the var-injection block).
|
|
7612
|
+
const isCrossRepoPlan = workType === WORK_TYPE.PLAN_TO_PRD
|
|
7613
|
+
&& !declaredPlanProject
|
|
7614
|
+
&& Array.isArray(planTargetProjects)
|
|
7615
|
+
&& planTargetProjects.length >= 2;
|
|
7616
|
+
let crossRepoFallbackProject = null;
|
|
7617
|
+
let crossRepoFallbackError = null;
|
|
7618
|
+
if (isCrossRepoPlan) {
|
|
7619
|
+
const firstResolution = shared.resolveConfiguredProject(planTargetProjects[0], projects);
|
|
7620
|
+
if (firstResolution.project) {
|
|
7621
|
+
crossRepoFallbackProject = firstResolution.project;
|
|
7622
|
+
} else if (firstResolution.error) {
|
|
7623
|
+
crossRepoFallbackError = firstResolution.error;
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7354
7626
|
const requestedProjectResolution = declaredPlanProject
|
|
7355
7627
|
? shared.resolveConfiguredProject(declaredPlanProject, projects)
|
|
7356
7628
|
: itemProjectResolution;
|
|
@@ -7365,7 +7637,22 @@ function discoverCentralWorkItems(config) {
|
|
|
7365
7637
|
log('warn', `central work item ${item.id}: ${error}`);
|
|
7366
7638
|
continue;
|
|
7367
7639
|
}
|
|
7368
|
-
|
|
7640
|
+
if (isCrossRepoPlan && crossRepoFallbackError && !crossRepoFallbackProject) {
|
|
7641
|
+
// First listed cross-repo target doesn't match any configured project
|
|
7642
|
+
// — fail with a clear reason instead of silently degrading to a
|
|
7643
|
+
// null targetProject and an empty vars.project_path.
|
|
7644
|
+
mutations.set(item.id, {
|
|
7645
|
+
status: WI_STATUS.FAILED,
|
|
7646
|
+
failReason: crossRepoFallbackError,
|
|
7647
|
+
failedAt: ts(),
|
|
7648
|
+
_crossRepoTargetProjects: planTargetProjects.slice(),
|
|
7649
|
+
});
|
|
7650
|
+
log('warn', `central work item ${item.id} (cross-repo plan-to-prd): ${crossRepoFallbackError}`);
|
|
7651
|
+
continue;
|
|
7652
|
+
}
|
|
7653
|
+
const targetProject = requestedProjectResolution.project
|
|
7654
|
+
|| crossRepoFallbackProject
|
|
7655
|
+
|| (projects.length === 1 ? projects[0] : null);
|
|
7369
7656
|
if (declaredPlanProject) {
|
|
7370
7657
|
const projectMutation = { project: targetProject.name, _declaredPlanProject: declaredPlanProject };
|
|
7371
7658
|
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, projectMutation));
|
|
@@ -7442,6 +7729,16 @@ function discoverCentralWorkItems(config) {
|
|
|
7442
7729
|
// Notes already populated by buildWorkItemDispatchVars — no need to re-read
|
|
7443
7730
|
// Track expected plan filename for artifacts and follow-up plan-to-prd prompts.
|
|
7444
7731
|
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _planFileName: planFileName }));
|
|
7732
|
+
// P-4d6e2af3 — surface cross-repo intent to the plan playbook.
|
|
7733
|
+
// dashboard.js#buildPlanWorkItem (~line 759) sets item._targetProjects
|
|
7734
|
+
// to a string[] when the operator picked ≥2 projects in the Create-
|
|
7735
|
+
// Plan modal. The playbook ({{target_projects}}, gated by P-c1f87a92)
|
|
7736
|
+
// renders a plural "Projects:" header + minions:targetProjects=… marker
|
|
7737
|
+
// so plan-to-prd can route each PRD item per-project. Single-project
|
|
7738
|
+
// plans leave the var unset; PLAYBOOK_OPTIONAL_VARS defaults it to ''.
|
|
7739
|
+
if (Array.isArray(item._targetProjects) && item._targetProjects.length > 0) {
|
|
7740
|
+
vars.target_projects = item._targetProjects.join(', ');
|
|
7741
|
+
}
|
|
7445
7742
|
}
|
|
7446
7743
|
|
|
7447
7744
|
// Inject plan-to-prd variables — read the plan file content for the playbook
|
|
@@ -7458,6 +7755,23 @@ function discoverCentralWorkItems(config) {
|
|
|
7458
7755
|
vars.plan_file = item.planFile || '';
|
|
7459
7756
|
vars.project_name_lower = (targetProject?.name || 'project').toLowerCase();
|
|
7460
7757
|
vars.project_filename_slug = safePrdProjectSlug(targetProject?.name || 'project');
|
|
7758
|
+
// P-9af2eb37 — cross-repo override. When the plan has NO singular
|
|
7759
|
+
// **Project:** header but ≥2 entries in the parsed target list,
|
|
7760
|
+
// (a) the per-project slug from safePrdProjectSlug would collide
|
|
7761
|
+
// across sibling cross-repo PRDs dispatched on the same day
|
|
7762
|
+
// (every cross-repo PRD on `minions-opg`-as-fallback would
|
|
7763
|
+
// resolve to `minions-opg-<date>.json`); use `cross-<uid>-…`
|
|
7764
|
+
// instead so each cross-repo PRD gets a unique filename and
|
|
7765
|
+
// the disambiguation in the existing prdExisting loop below
|
|
7766
|
+
// never has to disambiguate cross-repo siblings.
|
|
7767
|
+
// (b) surface the parsed list to the plan-to-prd.md playbook via
|
|
7768
|
+
// vars.target_projects so the agent can route each PRD item to
|
|
7769
|
+
// the right repo (PLAYBOOK_OPTIONAL_VARS already includes
|
|
7770
|
+
// target_projects — set in P-4d6e2af3 for the plan playbook).
|
|
7771
|
+
if (isCrossRepoPlan) {
|
|
7772
|
+
vars.project_filename_slug = 'cross-' + shared.uid();
|
|
7773
|
+
vars.target_projects = planTargetProjects.join(', ');
|
|
7774
|
+
}
|
|
7461
7775
|
// Default empty string so the {{existing_prd_json}} token always resolves —
|
|
7462
7776
|
// playbook treats empty as "no existing PRD, fresh run". Without this default
|
|
7463
7777
|
// the renderPlaybook pass logs an "unresolved template variables" warning
|
|
@@ -7603,91 +7917,116 @@ async function discoverWork(config) {
|
|
|
7603
7917
|
|
|
7604
7918
|
// Side-effect passes: materialize plans and design docs into work-items.json
|
|
7605
7919
|
// These write to project work queues — picked up by discoverFromWorkItems below.
|
|
7606
|
-
|
|
7607
|
-
|
|
7920
|
+
// Gated by config.engine?.planMaterializationEnabled !== false (P-d6f0a2b5) —
|
|
7921
|
+
// operators can suppress the PRD reconcile + plan materialization pair
|
|
7922
|
+
// without disabling the rest of discoverWork (e.g. when running a long
|
|
7923
|
+
// migration that must not auto-create work items).
|
|
7924
|
+
if (config.engine?.planMaterializationEnabled !== false) {
|
|
7925
|
+
reconcilePrdStatuses(config); // Backward-scan: correct "missing" PRD items that have done work items (#929)
|
|
7926
|
+
materializePlansAsWorkItems(config);
|
|
7927
|
+
}
|
|
7608
7928
|
|
|
7609
7929
|
for (const project of projects) {
|
|
7610
7930
|
const root = project.localPath ? path.resolve(project.localPath) : null;
|
|
7611
7931
|
if (!root || !fs.existsSync(root)) continue;
|
|
7612
7932
|
|
|
7613
7933
|
// Source 1: Pull Requests → fixes, reviews, build-test
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7934
|
+
// Gated by config.engine?.prDiscoveryEnabled !== false (P-d6f0a2b5) —
|
|
7935
|
+
// per-project project.workSources.pullRequests.enabled still composes
|
|
7936
|
+
// inside discoverFromPrs; a `false` at either level skips the call.
|
|
7937
|
+
if (config.engine?.prDiscoveryEnabled !== false) {
|
|
7938
|
+
const prWork = await discoverFromPrs(config, project);
|
|
7939
|
+
allFixes.push(...prWork.filter(w => w.type === WORK_TYPE.FIX));
|
|
7940
|
+
allReviews.push(...prWork.filter(w => w.type === WORK_TYPE.REVIEW));
|
|
7941
|
+
allWorkItems.push(...prWork.filter(w => w.type === WORK_TYPE.TEST));
|
|
7942
|
+
}
|
|
7618
7943
|
|
|
7619
7944
|
// Side-effect: specs → work items (picked up below)
|
|
7620
7945
|
materializeSpecsAsWorkItems(config, project);
|
|
7621
7946
|
|
|
7622
7947
|
// Source 3: Work items (includes auto-filed from plans, design docs, build failures)
|
|
7623
|
-
|
|
7948
|
+
// Gated by config.engine?.workItemsDiscoveryEnabled !== false (P-d6f0a2b5) —
|
|
7949
|
+
// per-project project.workSources.workItems.enabled still composes
|
|
7950
|
+
// inside discoverFromWorkItems; a `false` at either level skips the call.
|
|
7951
|
+
if (config.engine?.workItemsDiscoveryEnabled !== false) {
|
|
7952
|
+
allWorkItems.push(...discoverFromWorkItems(config, project));
|
|
7953
|
+
}
|
|
7624
7954
|
}
|
|
7625
7955
|
|
|
7626
7956
|
// Source 2: Minions-level PRD → implements (multi-project, called once outside project loop)
|
|
7627
7957
|
// PRD items (prd/*.json), materialized from plans/*.md, flow through materializePlansAsWorkItems → discoverFromWorkItems
|
|
7628
7958
|
|
|
7629
7959
|
// Central work items (project-agnostic — agent decides where to work)
|
|
7630
|
-
|
|
7960
|
+
// Gated by config.engine?.centralWorkDiscoveryEnabled !== false (P-d6f0a2b5) —
|
|
7961
|
+
// `let centralWork = []` keeps the downstream `...centralWork` spread
|
|
7962
|
+
// iterable when the gate is off.
|
|
7963
|
+
let centralWork = [];
|
|
7964
|
+
if (config.engine?.centralWorkDiscoveryEnabled !== false) {
|
|
7965
|
+
centralWork = discoverCentralWorkItems(config);
|
|
7966
|
+
}
|
|
7631
7967
|
|
|
7632
7968
|
// Scheduled tasks (cron-style recurring work)
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
const
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
if (taskItems.length > 0) {
|
|
7652
|
-
// Atomic write — prevents race with dispatch status updates on central work-items.json
|
|
7653
|
-
mutateJsonFileLocked(centralPath, (items) => {
|
|
7654
|
-
if (!Array.isArray(items)) items = [];
|
|
7655
|
-
let added = 0;
|
|
7656
|
-
// Snapshot active dedup keys BEFORE the loop so multiple items in the
|
|
7657
|
-
// same harness mission (same _missionId) all land in one tick. Without
|
|
7658
|
-
// this snapshot, the first item's push would block subsequent items
|
|
7659
|
-
// in the same mission from joining (W-mq07a9gf000jbc2b — tri-agent
|
|
7660
|
-
// harness mode requires Planner+Generator+Evaluator to land together).
|
|
7661
|
-
const activeMissionIds = new Set();
|
|
7662
|
-
const activeScheduleIds = new Set();
|
|
7663
|
-
for (const existing of items) {
|
|
7664
|
-
if (existing.status === WI_STATUS.DONE || existing.status === WI_STATUS.FAILED) continue;
|
|
7665
|
-
if (existing._missionId) activeMissionIds.add(existing._missionId);
|
|
7666
|
-
if (existing._scheduleId) activeScheduleIds.add(existing._scheduleId);
|
|
7969
|
+
// Gated by config.engine?.scheduledWorkDiscoveryEnabled !== false (P-d6f0a2b5).
|
|
7970
|
+
if (config.engine?.scheduledWorkDiscoveryEnabled !== false) {
|
|
7971
|
+
try {
|
|
7972
|
+
const { discoverScheduledWork } = require('./engine/scheduler');
|
|
7973
|
+
const scheduledWork = discoverScheduledWork(config);
|
|
7974
|
+
if (scheduledWork.length > 0) {
|
|
7975
|
+
const { createMeeting, getMeetings } = require('./engine/meeting');
|
|
7976
|
+
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
7977
|
+
// Separate meetings (no work-items write) from task items
|
|
7978
|
+
const taskItems = [];
|
|
7979
|
+
for (const item of scheduledWork) {
|
|
7980
|
+
if (item.type === WORK_TYPE.MEETING) {
|
|
7981
|
+
const sched = (config.schedules || []).find(s => s.id === item._scheduleId);
|
|
7982
|
+
const participants = (sched && sched.participants) || [];
|
|
7983
|
+
const meeting = createMeeting({ title: item.title, agenda: item.description, participants });
|
|
7984
|
+
log('info', `Scheduled meeting created: ${item._scheduleId} → ${meeting.id} (${participants.length} participants)`);
|
|
7985
|
+
} else {
|
|
7986
|
+
taskItems.push(item);
|
|
7667
7987
|
}
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
//
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7988
|
+
}
|
|
7989
|
+
if (taskItems.length > 0) {
|
|
7990
|
+
// Atomic write — prevents race with dispatch status updates on central work-items.json
|
|
7991
|
+
mutateJsonFileLocked(centralPath, (items) => {
|
|
7992
|
+
if (!Array.isArray(items)) items = [];
|
|
7993
|
+
let added = 0;
|
|
7994
|
+
// Snapshot active dedup keys BEFORE the loop so multiple items in the
|
|
7995
|
+
// same harness mission (same _missionId) all land in one tick. Without
|
|
7996
|
+
// this snapshot, the first item's push would block subsequent items
|
|
7997
|
+
// in the same mission from joining (W-mq07a9gf000jbc2b — tri-agent
|
|
7998
|
+
// harness mode requires Planner+Generator+Evaluator to land together).
|
|
7999
|
+
const activeMissionIds = new Set();
|
|
8000
|
+
const activeScheduleIds = new Set();
|
|
8001
|
+
for (const existing of items) {
|
|
8002
|
+
if (existing.status === WI_STATUS.DONE || existing.status === WI_STATUS.FAILED) continue;
|
|
8003
|
+
if (existing._missionId) activeMissionIds.add(existing._missionId);
|
|
8004
|
+
if (existing._scheduleId) activeScheduleIds.add(existing._scheduleId);
|
|
7680
8005
|
}
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
8006
|
+
const addedScheduleIdsThisTick = new Set();
|
|
8007
|
+
for (const item of taskItems) {
|
|
8008
|
+
// Mission items dedup by _missionId against pre-existing rows only
|
|
8009
|
+
// (the trio's other items added later in this loop must not block
|
|
8010
|
+
// each other). Plain scheduled items keep the original scheduleId
|
|
8011
|
+
// dedup AND skip if a sibling item from the same tick already
|
|
8012
|
+
// claimed the schedule slot.
|
|
8013
|
+
if (item._missionId) {
|
|
8014
|
+
if (activeMissionIds.has(item._missionId)) continue;
|
|
8015
|
+
} else {
|
|
8016
|
+
if (activeScheduleIds.has(item._scheduleId)) continue;
|
|
8017
|
+
if (addedScheduleIdsThisTick.has(item._scheduleId)) continue;
|
|
8018
|
+
}
|
|
8019
|
+
items.push(item);
|
|
8020
|
+
if (!item._missionId && item._scheduleId) addedScheduleIdsThisTick.add(item._scheduleId);
|
|
8021
|
+
added++;
|
|
8022
|
+
log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
|
|
8023
|
+
}
|
|
8024
|
+
return items;
|
|
8025
|
+
}, { defaultValue: [] });
|
|
8026
|
+
}
|
|
7688
8027
|
}
|
|
7689
|
-
}
|
|
7690
|
-
}
|
|
8028
|
+
} catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
|
|
8029
|
+
}
|
|
7691
8030
|
|
|
7692
8031
|
// Meeting work (multi-round team discussions)
|
|
7693
8032
|
try {
|
|
@@ -7768,15 +8107,37 @@ async function discoverWork(config) {
|
|
|
7768
8107
|
|
|
7769
8108
|
const allWork = [...allFixes, ...allReviews, ...allWorkItems, ...centralWork];
|
|
7770
8109
|
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
8110
|
+
// W-mq9acoo800177bcb — bounded-concurrency pre-dispatch eval.
|
|
8111
|
+
// The validator inside addToDispatchWithValidation runs a ~25-30s LLM
|
|
8112
|
+
// call per item; an approved 11-item PRD used to take ~5 min to queue
|
|
8113
|
+
// because we awaited each item in series. Process items in chunks of
|
|
8114
|
+
// `preDispatchEvalConcurrency` (default 6, clamp 1-20) via
|
|
8115
|
+
// Promise.allSettled so a rejected branch in one item still fails open
|
|
8116
|
+
// and the rest of the chunk continues. Iteration order within a chunk
|
|
8117
|
+
// is irrelevant: addToDispatch's dedup gates are mutateDispatch-locked,
|
|
8118
|
+
// and the only per-item side effect (`clearPendingHumanFeedbackFlag`)
|
|
8119
|
+
// is an idempotent flag-clear on a single PR record.
|
|
8120
|
+
const evalConcurrency = resolvePreDispatchEvalConcurrency(config);
|
|
8121
|
+
for (let i = 0; i < allWork.length; i += evalConcurrency) {
|
|
8122
|
+
const chunk = allWork.slice(i, i + evalConcurrency);
|
|
8123
|
+
await Promise.allSettled(chunk.map(async (item) => {
|
|
8124
|
+
try {
|
|
8125
|
+
await addToDispatchWithValidation(item, { config });
|
|
8126
|
+
} catch (e) {
|
|
8127
|
+
// Fail-open contract preserved: any validator throw here means the
|
|
8128
|
+
// item is dropped from this discovery tick rather than wedging the
|
|
8129
|
+
// whole chunk. The item stays `pending` in work-items.json and the
|
|
8130
|
+
// next tick re-discovers it.
|
|
8131
|
+
log('warn', `pre-dispatch-eval: addToDispatchWithValidation threw for ${item?.meta?.item?.id || item?.id || 'unknown'}: ${e?.message || e}`);
|
|
8132
|
+
}
|
|
8133
|
+
// W-mph8xt88000ke0fc — Cooldowns are stamped by addToDispatch ONLY on
|
|
8134
|
+
// successful append (post-dedup). Stamping unconditionally here used to
|
|
8135
|
+
// leave orphan cooldowns for items collapsed by prDedupeKey / workItem-id
|
|
8136
|
+
// / dispatchKey dedup or routed to the pre-dispatch review queue.
|
|
8137
|
+
if (item.meta?.source === 'pr-human-feedback') {
|
|
8138
|
+
clearPendingHumanFeedbackFlag(item.meta.project, item.meta.pr?.id);
|
|
8139
|
+
}
|
|
8140
|
+
}));
|
|
7780
8141
|
}
|
|
7781
8142
|
|
|
7782
8143
|
if (allWork.length > 0) {
|
|
@@ -7873,6 +8234,20 @@ function resolveMaxConcurrent(config) {
|
|
|
7873
8234
|
return Number.isFinite(value) && value >= 0 ? value : ENGINE_DEFAULTS.maxConcurrent;
|
|
7874
8235
|
}
|
|
7875
8236
|
|
|
8237
|
+
// W-mq9acoo800177bcb — clamp the per-tick pre-dispatch validator concurrency.
|
|
8238
|
+
// Read-side mirror of the [1, 20] clamp dashboard.js#handleSettingsUpdate
|
|
8239
|
+
// applies on writes, so a hand-edited config.json or an engine-only test
|
|
8240
|
+
// fixture can't smuggle in a value that DOSes the LLM provider or starves
|
|
8241
|
+
// the rest of the tick. Falls back to ENGINE_DEFAULTS.preDispatchEvalConcurrency
|
|
8242
|
+
// for unset / non-finite / out-of-range values.
|
|
8243
|
+
function resolvePreDispatchEvalConcurrency(config) {
|
|
8244
|
+
const raw = config?.engine?.preDispatchEvalConcurrency;
|
|
8245
|
+
if (raw === undefined || raw === null || raw === '') return ENGINE_DEFAULTS.preDispatchEvalConcurrency;
|
|
8246
|
+
const value = Number(raw);
|
|
8247
|
+
if (!Number.isFinite(value)) return ENGINE_DEFAULTS.preDispatchEvalConcurrency;
|
|
8248
|
+
return Math.max(1, Math.min(20, Math.floor(value)));
|
|
8249
|
+
}
|
|
8250
|
+
|
|
7876
8251
|
// ─── Main Tick ──────────────────────────────────────────────────────────────
|
|
7877
8252
|
|
|
7878
8253
|
let tickCount = 0;
|
|
@@ -8152,8 +8527,45 @@ async function tickInner() {
|
|
|
8152
8527
|
});
|
|
8153
8528
|
}
|
|
8154
8529
|
|
|
8155
|
-
|
|
8156
|
-
|
|
8530
|
+
// P-a1f3c2d4: engine.pollingPaused is a hard-stop master switch — it overrides
|
|
8531
|
+
// both per-provider toggles. When ON, adoPollEnabled and ghPollEnabled are
|
|
8532
|
+
// forced to false so the status-poll and human-comment-poll gates below
|
|
8533
|
+
// short-circuit without editing every per-call gate. Reconciliation (the
|
|
8534
|
+
// final block below) is NOT gated — it's a recovery sweep, not a
|
|
8535
|
+
// convenience poll. The same gate is mirrored inside discoverFromPrs.
|
|
8536
|
+
const pollingPaused = config.engine?.pollingPaused === true;
|
|
8537
|
+
if (pollingPaused && _shouldLogPollingPausedOnce()) {
|
|
8538
|
+
log('info', '[engine] PR polling paused — engine.pollingPaused=true overrides adoPollEnabled/ghPollEnabled');
|
|
8539
|
+
} else if (!pollingPaused) {
|
|
8540
|
+
_resetPollingPausedLogState();
|
|
8541
|
+
}
|
|
8542
|
+
// P-b2e5d8c7: log once on the transition unpaused → paused for autoFixPaused
|
|
8543
|
+
// too. The gate itself is enforced inside discoverFromPrs (composed into
|
|
8544
|
+
// autoFixBuilds / autoFixConflicts / autoFixReviewFeedback /
|
|
8545
|
+
// autoFixHumanComments). Same throttling contract as pollingPaused above.
|
|
8546
|
+
const autoFixPaused = config.engine?.autoFixPaused === true;
|
|
8547
|
+
if (autoFixPaused && _shouldLogAutoFixPausedOnce()) {
|
|
8548
|
+
log('info', '[engine] auto-fix paused — engine.autoFixPaused=true inerts autoFixBuilds/autoFixConflicts/autoFixReviewFeedback/autoFixHumanComments');
|
|
8549
|
+
} else if (!autoFixPaused) {
|
|
8550
|
+
_resetAutoFixPausedLogState();
|
|
8551
|
+
}
|
|
8552
|
+
const adoPollEnabled = !pollingPaused && (config.engine?.adoPollEnabled ?? ENGINE_DEFAULTS.adoPollEnabled);
|
|
8553
|
+
const ghPollEnabled = !pollingPaused && (config.engine?.ghPollEnabled ?? ENGINE_DEFAULTS.ghPollEnabled);
|
|
8554
|
+
// P-c4d8e1a3 — granular per-poller flags. Each of the 6 PR-poll axes
|
|
8555
|
+
// (ADO/GH × status/comments/reconcile) plus processPendingRebases gets
|
|
8556
|
+
// its own resolution via shared.resolvePollFlag(...). Explicit granular
|
|
8557
|
+
// values win; the legacy `adoPollEnabled` / `ghPollEnabled` macros
|
|
8558
|
+
// silence their whole bundle as a fallback (including reconcile — a
|
|
8559
|
+
// documented breaking change). Defaults: all true. pollingPaused (master
|
|
8560
|
+
// killswitch) still composes with status + comments gates; reconcile and
|
|
8561
|
+
// the rebase processor are recovery-sweep paths and are NOT gated by it.
|
|
8562
|
+
const adoStatusPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'adoPrStatusPollEnabled', 'adoPollEnabled');
|
|
8563
|
+
const ghStatusPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'ghPrStatusPollEnabled', 'ghPollEnabled');
|
|
8564
|
+
const adoCommentsPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'adoPrCommentsPollEnabled', 'adoPollEnabled');
|
|
8565
|
+
const ghCommentsPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'ghPrCommentsPollEnabled', 'ghPollEnabled');
|
|
8566
|
+
const adoReconcileEnabled = resolvePollFlag(config.engine, 'adoPrReconcileEnabled', 'adoPollEnabled');
|
|
8567
|
+
const ghReconcileEnabled = resolvePollFlag(config.engine, 'ghPrReconcileEnabled', 'ghPollEnabled');
|
|
8568
|
+
const rebaseProcessorEnabled = resolvePollFlag(config.engine, 'processPendingRebasesEnabled', null);
|
|
8157
8569
|
const prPollStatusEvery = Math.max(
|
|
8158
8570
|
1,
|
|
8159
8571
|
Number(config.engine?.prPollStatusEvery) || ENGINE_DEFAULTS.prPollStatusEvery
|
|
@@ -8173,7 +8585,7 @@ async function tickInner() {
|
|
|
8173
8585
|
lastPrStatusPollAt = now;
|
|
8174
8586
|
// Build promise array — enabled+unthrottled polls run concurrently via Promise.allSettled
|
|
8175
8587
|
const statusPolls = [];
|
|
8176
|
-
if (
|
|
8588
|
+
if (adoStatusPollEnabled) {
|
|
8177
8589
|
// Per-org throttle skip happens inside forEachActivePr (one log line per skipped project).
|
|
8178
8590
|
// Top-level short-circuit: when every known ADO org is throttled, skip the whole phase
|
|
8179
8591
|
// with one log line to avoid the per-project iteration cost.
|
|
@@ -8186,14 +8598,16 @@ async function tickInner() {
|
|
|
8186
8598
|
statusPolls.push(pollPrStatus(config).catch(err => { log('warn', `ADO PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
8187
8599
|
}
|
|
8188
8600
|
}
|
|
8189
|
-
if (
|
|
8601
|
+
if (ghStatusPollEnabled && !isGhThrottled()) {
|
|
8190
8602
|
statusPolls.push(ghPollPrStatus(config).catch(err => { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
8191
|
-
} else if (
|
|
8603
|
+
} else if (ghStatusPollEnabled && isGhThrottled()) {
|
|
8192
8604
|
log('info', '[gh] PR status poll skipped — throttled');
|
|
8193
8605
|
}
|
|
8194
8606
|
if (statusPolls.length) await Promise.allSettled(statusPolls);
|
|
8195
8607
|
if (_isTickStale(myGeneration)) return;
|
|
8196
|
-
|
|
8608
|
+
if (rebaseProcessorEnabled) {
|
|
8609
|
+
try { await processPendingRebases(config); } catch (err) { log('warn', `Pending rebase processing error: ${err?.message || err}`); }
|
|
8610
|
+
}
|
|
8197
8611
|
if (_isTickStale(myGeneration)) return;
|
|
8198
8612
|
// Sync PR status back to PRD items (missing → done when active PR exists)
|
|
8199
8613
|
try { syncPrdFromPrs(config); } catch (err) { log('warn', `PRD sync error: ${err?.message || err}`); }
|
|
@@ -8227,7 +8641,7 @@ async function tickInner() {
|
|
|
8227
8641
|
lastPrCommentsPollAt = now;
|
|
8228
8642
|
// Build promise array — enabled+unthrottled comment polls run concurrently via Promise.allSettled
|
|
8229
8643
|
const commentPolls = [];
|
|
8230
|
-
if (
|
|
8644
|
+
if (adoCommentsPollEnabled) {
|
|
8231
8645
|
// Per-org throttle skip happens inside forEachActivePr (one log line per skipped project).
|
|
8232
8646
|
// Top-level short-circuit: when every known ADO org is throttled, skip the whole phase
|
|
8233
8647
|
// with one log line to avoid the per-project iteration cost.
|
|
@@ -8240,20 +8654,41 @@ async function tickInner() {
|
|
|
8240
8654
|
commentPolls.push(pollPrHumanComments(config).catch(err => { log('warn', `ADO PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
8241
8655
|
}
|
|
8242
8656
|
}
|
|
8243
|
-
if (
|
|
8657
|
+
if (ghCommentsPollEnabled && !isGhThrottled()) {
|
|
8244
8658
|
commentPolls.push(ghPollPrHumanComments(config).catch(err => { log('warn', `GitHub PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
8245
|
-
} else if (
|
|
8659
|
+
} else if (ghCommentsPollEnabled && isGhThrottled()) {
|
|
8246
8660
|
log('info', '[gh] PR comment poll skipped — throttled');
|
|
8247
8661
|
}
|
|
8248
8662
|
if (commentPolls.length) await Promise.allSettled(commentPolls);
|
|
8249
8663
|
if (_isTickStale(myGeneration)) return;
|
|
8250
|
-
//
|
|
8251
|
-
//
|
|
8664
|
+
// P-c4d8e1a3: reconcile gated by *PrReconcileEnabled (default true) — ungated
|
|
8665
|
+
// by ADO/GH throttle trackers (recovery-sweep contract intact). Legacy
|
|
8666
|
+
// adoPollEnabled/ghPollEnabled=false now also silences reconcile via
|
|
8667
|
+
// resolvePollFlag's legacy-macro fallback (documented breaking change).
|
|
8668
|
+
// ADO and GitHub reconciliation are independent and run in parallel.
|
|
8252
8669
|
const reconcilePolls = [];
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8670
|
+
if (adoReconcileEnabled) {
|
|
8671
|
+
reconcilePolls.push(reconcilePrs(config).catch(err => { log('warn', `ADO PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
8672
|
+
}
|
|
8673
|
+
if (ghReconcileEnabled) {
|
|
8674
|
+
reconcilePolls.push(ghReconcilePrs(config).catch(err => { log('warn', `GitHub PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
8675
|
+
}
|
|
8676
|
+
if (reconcilePolls.length) await Promise.allSettled(reconcilePolls);
|
|
8256
8677
|
if (_isTickStale(myGeneration)) return;
|
|
8678
|
+
|
|
8679
|
+
// W-mqba5ulq000nd255 — Cross-project PR scope-mismatch sweep. Cleans up
|
|
8680
|
+
// stale records that were stamped with `_invalidProjectScope` and have a
|
|
8681
|
+
// sibling in the correctly-scoped project. Cheap (a single read of all
|
|
8682
|
+
// PRs + a per-affected-project mutation). Runs once per reconcile tick;
|
|
8683
|
+
// sibling-less stamps are preserved as tracking signals.
|
|
8684
|
+
try {
|
|
8685
|
+
const result = pruneScopeMismatchDuplicatePrs(config);
|
|
8686
|
+
if (result?.pruned > 0) {
|
|
8687
|
+
log('info', `[pull-requests] scope-mismatch sweep pruned ${result.pruned} duplicate record(s) across ${result.scanned} scanned`);
|
|
8688
|
+
}
|
|
8689
|
+
} catch (err) {
|
|
8690
|
+
log('warn', `[pull-requests] scope-mismatch sweep error: ${err?.message || err}`);
|
|
8691
|
+
}
|
|
8257
8692
|
}
|
|
8258
8693
|
|
|
8259
8694
|
// 2.9. Stalled dispatch detection — auto-retry failed items blocking the graph (~20 min — cadence in ENGINE_DEFAULTS.stalledDispatchSweepEvery)
|
|
@@ -8793,6 +9228,29 @@ async function tickInner() {
|
|
|
8793
9228
|
if (!discoveryOk) {
|
|
8794
9229
|
log('warn', 'Discovery failed after pending dispatch pass — new work may be stale until next tick');
|
|
8795
9230
|
}
|
|
9231
|
+
|
|
9232
|
+
// 6. Periodic memory baseline (P-b2c3d4e5).
|
|
9233
|
+
// Sample once every ENGINE_DEFAULTS.memoryBaselineEveryTicks ticks (default
|
|
9234
|
+
// 6 ≈ 60s at the default 10s tickInterval). When the cadence is <= 0,
|
|
9235
|
+
// sampling is disabled cleanly — no log emission, no sidecar write
|
|
9236
|
+
// (operator opt-out). The diagnostics-memory.json sidecar is a passive
|
|
9237
|
+
// cache (single-object latest sample) — gitignored and exempt from the
|
|
9238
|
+
// SQL-first state rule, same as engine/dashboard-port.json.
|
|
9239
|
+
safe('memoryBaseline', () => emitMemoryBaseline(tickCount));
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9242
|
+
function emitMemoryBaseline(tickN) {
|
|
9243
|
+
const every = Number(ENGINE_DEFAULTS.memoryBaselineEveryTicks);
|
|
9244
|
+
if (!Number.isFinite(every) || every <= 0) return;
|
|
9245
|
+
if ((tickN % every) !== 0) return;
|
|
9246
|
+
const sample = diagnosticsMemory.sampleSelf({ label: 'engine' });
|
|
9247
|
+
diagnosticsMemory.recordSample(sample);
|
|
9248
|
+
try { safeWrite(DIAGNOSTICS_MEMORY_PATH, sample); }
|
|
9249
|
+
catch (e) { log('warn', `memoryBaseline sidecar write: ${e.message}`); }
|
|
9250
|
+
log('info',
|
|
9251
|
+
`MEMORY_BASELINE engine rss=${sample.rss} heapUsed=${sample.heapUsed} ` +
|
|
9252
|
+
`eventLoopLagP99=${sample.eventLoopLagP99.toFixed(2)}ms ` +
|
|
9253
|
+
`gcPauses=${sample.gcCount}/${sample.gcPausesTotalMs.toFixed(2)}ms tickN=${tickN}`);
|
|
8796
9254
|
}
|
|
8797
9255
|
|
|
8798
9256
|
// ─── Exports (for engine/cli.js and other modules) ──────────────────────────
|
|
@@ -8832,6 +9290,7 @@ module.exports = {
|
|
|
8832
9290
|
reconcileItemsWithPrs, detectDependencyCycles,
|
|
8833
9291
|
areDependenciesMet, // exported for testing (P-bf04-decompose-zero-children)
|
|
8834
9292
|
parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
|
|
9293
|
+
resolveDependencyBranches, buildCrossRepoDepsSection, // exported for testing (P-faea3206)
|
|
8835
9294
|
gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
|
|
8836
9295
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
8837
9296
|
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
@@ -8866,8 +9325,12 @@ module.exports = {
|
|
|
8866
9325
|
// Tick
|
|
8867
9326
|
tick,
|
|
8868
9327
|
resolveMaxConcurrent, _pollIntervalMsFromTicks, _shouldRunPeriodicPhase, // exported for testing
|
|
9328
|
+
// W-mq9acoo800177bcb — exported for testing the clamped concurrency resolver
|
|
9329
|
+
resolvePreDispatchEvalConcurrency,
|
|
8869
9330
|
// P-c2e5a1d9-a — exported for testing the tick-generation force-release path
|
|
8870
9331
|
_isTickStale,
|
|
9332
|
+
// P-b2c3d4e5 — exported for testing the memory baseline emitter + sidecar path
|
|
9333
|
+
emitMemoryBaseline, DIAGNOSTICS_MEMORY_PATH,
|
|
8871
9334
|
get tickGeneration() { return tickGeneration; },
|
|
8872
9335
|
set tickGeneration(v) { tickGeneration = v; },
|
|
8873
9336
|
get tickRunning() { return tickRunning; },
|