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