@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/lifecycle.js
CHANGED
|
@@ -1472,7 +1472,14 @@ async function findOpenPrForBranch(meta, config) {
|
|
|
1472
1472
|
if (!meta?.branch) return null;
|
|
1473
1473
|
const projectObj = resolvePrFallbackProject(meta, config);
|
|
1474
1474
|
if (!projectObj) return null;
|
|
1475
|
-
|
|
1475
|
+
// BUG-H14 (P-h14-repohost): no silent 'ado' default — if the resolved
|
|
1476
|
+
// project doesn't declare repoHost, bail rather than mis-routing a GitHub
|
|
1477
|
+
// project's branch lookup through the ADO PR-search path.
|
|
1478
|
+
const host = projectObj.repoHost || null;
|
|
1479
|
+
if (!host) {
|
|
1480
|
+
log('debug', `Skipping branch PR lookup: project ${projectObj.name} has no repoHost configured`);
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1476
1483
|
if (host === 'github') {
|
|
1477
1484
|
const ghSlug = projectObj.prUrlBase?.match(/github\.com\/([^/]+\/[^/]+)\/pull/)?.[1];
|
|
1478
1485
|
if (!ghSlug) return null;
|
|
@@ -1981,13 +1988,26 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1981
1988
|
// The poller will pick up the real status on the next cycle (~3 min).
|
|
1982
1989
|
let postReviewStatus = null; // null = don't change
|
|
1983
1990
|
let liveStatus = null;
|
|
1991
|
+
// BUG-H14 (P-h14-repohost): resolve repoHost from the PR's canonical id
|
|
1992
|
+
// FIRST. The previous code derived hostForChecks from the fallback project
|
|
1993
|
+
// (`reviewProject || getProjects(config)[0]`) and silently defaulted to
|
|
1994
|
+
// 'ado' on miss — so a github:* PR found in the central pull-requests.json
|
|
1995
|
+
// (reviewProject=null) with an ADO project as projects[0] was mis-routed to
|
|
1996
|
+
// ado.checkLiveReviewStatus. The fallback project is still consulted when
|
|
1997
|
+
// the id is unparseable, but there is no silent 'ado' default — an
|
|
1998
|
+
// unresolved host is observable (no live check fires).
|
|
1984
1999
|
const projectObjForChecks = reviewProject || shared.getProjects(config)[0];
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
1987
|
-
?
|
|
1988
|
-
:
|
|
2000
|
+
const parsedCanonicalReviewId = shared.parseCanonicalPrId(reviewPr.id);
|
|
2001
|
+
const hostFromCanonical = parsedCanonicalReviewId
|
|
2002
|
+
? parsedCanonicalReviewId.scope.split(':')[0]
|
|
2003
|
+
: null;
|
|
2004
|
+
const hostForChecks = hostFromCanonical || projectObjForChecks?.repoHost || null;
|
|
2005
|
+
let checkFn = null;
|
|
2006
|
+
if (hostForChecks === 'github') checkFn = require('./github').checkLiveReviewStatus;
|
|
2007
|
+
else if (hostForChecks === 'ado') checkFn = require('./ado').checkLiveReviewStatus;
|
|
2008
|
+
else log('warn', `Cannot resolve repoHost for review of ${reviewPr.id} (unparseable canonical id and no project config); skipping live review check.`);
|
|
1989
2009
|
try {
|
|
1990
|
-
if (projectObjForChecks) {
|
|
2010
|
+
if (projectObjForChecks && checkFn) {
|
|
1991
2011
|
liveStatus = await checkFn(reviewPr, projectObjForChecks);
|
|
1992
2012
|
}
|
|
1993
2013
|
} catch (e) { log('warn', `Post-review status check for ${reviewPr.id}: ${e.message}`); }
|
|
@@ -2025,7 +2045,9 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2025
2045
|
try {
|
|
2026
2046
|
const reconcileFn = hostForChecks === 'github'
|
|
2027
2047
|
? require('./github').dismissPriorViewerChangesRequestedReviews
|
|
2028
|
-
:
|
|
2048
|
+
: hostForChecks === 'ado'
|
|
2049
|
+
? require('./ado').resetReviewerNegativeVote
|
|
2050
|
+
: null;
|
|
2029
2051
|
if (typeof reconcileFn === 'function') {
|
|
2030
2052
|
const result = await reconcileFn(reviewPr, projectObjForChecks);
|
|
2031
2053
|
const cleared = !!(result && (result.changed || result.dismissed > 0));
|
|
@@ -3395,85 +3417,6 @@ function parseAgentOutput(stdout, runtimeName) {
|
|
|
3395
3417
|
return { resultSummary: text, taskUsage: usage, sessionId, model };
|
|
3396
3418
|
}
|
|
3397
3419
|
|
|
3398
|
-
/**
|
|
3399
|
-
* Parse structured completion block from agent output.
|
|
3400
|
-
* Agents produce a ```completion fenced block with key: value pairs.
|
|
3401
|
-
* Returns parsed object or null if not found / malformed.
|
|
3402
|
-
* If multiple blocks exist, the last one wins (agent may retry).
|
|
3403
|
-
*
|
|
3404
|
-
* DEPRECATED — slated for removal once telemetry shows zero hits over a 14-day
|
|
3405
|
-
* window. Telemetry: see _engine.completionFallbacks in metrics.json (the JSON
|
|
3406
|
-
* sidecar at MINIONS_COMPLETION_REPORT is the documented contract; this fenced
|
|
3407
|
-
* fallback exists only to support runtimes/agents that have not yet adopted it).
|
|
3408
|
-
* Removal is tracked by the P-c8f5e1b3 follow-up plan.
|
|
3409
|
-
*/
|
|
3410
|
-
function parseStructuredCompletion(stdout, runtimeName) {
|
|
3411
|
-
if (!stdout || typeof stdout !== 'string') return null;
|
|
3412
|
-
|
|
3413
|
-
// Extract text from stream-json output if needed
|
|
3414
|
-
const text = extractCompletionText(stdout, runtimeName);
|
|
3415
|
-
|
|
3416
|
-
// Find all ```completion blocks, take the last one
|
|
3417
|
-
const blockPattern = /```completion\s*\n([\s\S]*?)```/g;
|
|
3418
|
-
let lastMatch = null;
|
|
3419
|
-
let m;
|
|
3420
|
-
while ((m = blockPattern.exec(text)) !== null) {
|
|
3421
|
-
lastMatch = m[1];
|
|
3422
|
-
}
|
|
3423
|
-
if (!lastMatch) {
|
|
3424
|
-
const taskCompleteSummary = extractTaskCompleteSummary(stdout);
|
|
3425
|
-
return taskCompleteSummary ? parseCompletionKeyValues(taskCompleteSummary) : null;
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
return parseCompletionKeyValues(lastMatch);
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
|
-
function extractCompletionText(stdout, runtimeName) {
|
|
3432
|
-
let text = stdout;
|
|
3433
|
-
if (typeof stdout === 'string' && stdout.includes('"type":')) {
|
|
3434
|
-
try {
|
|
3435
|
-
const parsed = shared.parseStreamJsonOutput(stdout, runtimeName);
|
|
3436
|
-
if (parsed.text) text = parsed.text;
|
|
3437
|
-
} catch {}
|
|
3438
|
-
}
|
|
3439
|
-
return text;
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
function hasCompletionFence(stdout, runtimeName) {
|
|
3443
|
-
const text = extractCompletionText(stdout, runtimeName);
|
|
3444
|
-
return /```completion\s*\n[\s\S]*?```/.test(text);
|
|
3445
|
-
}
|
|
3446
|
-
|
|
3447
|
-
function extractTaskCompleteSummary(stdout) {
|
|
3448
|
-
if (!stdout || typeof stdout !== 'string') return '';
|
|
3449
|
-
let summary = '';
|
|
3450
|
-
for (const rawLine of stdout.split('\n')) {
|
|
3451
|
-
const line = rawLine.trim();
|
|
3452
|
-
if (!line || !line.startsWith('{')) continue;
|
|
3453
|
-
let obj;
|
|
3454
|
-
try { obj = JSON.parse(line); } catch { continue; }
|
|
3455
|
-
if (!obj || typeof obj !== 'object') continue;
|
|
3456
|
-
if (obj.type === 'session.task_complete') {
|
|
3457
|
-
const value = obj.data?.summary;
|
|
3458
|
-
if (typeof value === 'string' && value.trim()) summary = value;
|
|
3459
|
-
continue;
|
|
3460
|
-
}
|
|
3461
|
-
if (obj.type === 'tool.execution_start' && obj.data?.toolName === 'task_complete') {
|
|
3462
|
-
const value = obj.data?.arguments?.summary;
|
|
3463
|
-
if (typeof value === 'string' && value.trim()) summary = value;
|
|
3464
|
-
continue;
|
|
3465
|
-
}
|
|
3466
|
-
if (obj.type === 'assistant.message' && Array.isArray(obj.data?.toolRequests)) {
|
|
3467
|
-
for (const tr of obj.data.toolRequests) {
|
|
3468
|
-
if (tr?.name !== 'task_complete') continue;
|
|
3469
|
-
const value = tr.arguments?.summary || tr.intentionSummary;
|
|
3470
|
-
if (typeof value === 'string' && value.trim()) summary = value;
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
return summary;
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
3420
|
function hasActionableFailureClass(value) {
|
|
3478
3421
|
const normalized = String(value || '').trim().toLowerCase();
|
|
3479
3422
|
if (!normalized) return false;
|
|
@@ -3577,57 +3520,6 @@ function handleInjectionFlag(dispatchItem, agentId, structuredCompletion, config
|
|
|
3577
3520
|
return { description, sources, at };
|
|
3578
3521
|
}
|
|
3579
3522
|
|
|
3580
|
-
function parseCompletionKeyValues(text) {
|
|
3581
|
-
if (!text || typeof text !== 'string') return null;
|
|
3582
|
-
const result = {};
|
|
3583
|
-
const allowedFields = new Set(shared.COMPLETION_FIELDS || []);
|
|
3584
|
-
const lines = text.trim().split('\n');
|
|
3585
|
-
for (const line of lines) {
|
|
3586
|
-
const normalizedLine = line.trim().replace(/^[-*]\s+/, '');
|
|
3587
|
-
const colonIdx = normalizedLine.indexOf(':');
|
|
3588
|
-
if (colonIdx < 1) continue;
|
|
3589
|
-
const key = normalizedLine.slice(0, colonIdx).trim().toLowerCase();
|
|
3590
|
-
if (allowedFields.size > 0 && !allowedFields.has(key)) continue;
|
|
3591
|
-
const value = normalizedLine.slice(colonIdx + 1).trim();
|
|
3592
|
-
if (key && value) result[key] = value;
|
|
3593
|
-
}
|
|
3594
|
-
|
|
3595
|
-
// Must have at least status, or an actionable failure_class that implies failure.
|
|
3596
|
-
if (!result.status && hasActionableFailureClass(result.failure_class)) result.status = 'failed';
|
|
3597
|
-
if (!result.status) return null;
|
|
3598
|
-
return result;
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
|
-
/**
|
|
3602
|
-
* DEPRECATED — slated for removal once telemetry shows zero hits over a 14-day
|
|
3603
|
-
* window. Telemetry: see _engine.completionFallbacks in metrics.json (the JSON
|
|
3604
|
-
* sidecar at MINIONS_COMPLETION_REPORT is the documented contract; this prose
|
|
3605
|
-
* fallback only fires when both the sidecar AND the fenced ```completion block
|
|
3606
|
-
* are missing). Removal is tracked by the P-c8f5e1b3 follow-up plan.
|
|
3607
|
-
*/
|
|
3608
|
-
function parseCompletionFieldSummary(text) {
|
|
3609
|
-
if (!text || typeof text !== 'string') return null;
|
|
3610
|
-
|
|
3611
|
-
const allowedFields = new Set(shared.COMPLETION_FIELDS || []);
|
|
3612
|
-
const result = {};
|
|
3613
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
3614
|
-
const line = rawLine.trim().replace(/^[-*]\s+/, '');
|
|
3615
|
-
const colonIdx = line.indexOf(':');
|
|
3616
|
-
if (colonIdx < 1) continue;
|
|
3617
|
-
const key = line.slice(0, colonIdx).trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
3618
|
-
if (!allowedFields.has(key)) continue;
|
|
3619
|
-
const value = line.slice(colonIdx + 1).trim().replace(/^["'`]+|["'`]+$/g, '');
|
|
3620
|
-
if (value) result[key] = value;
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
if (!result.status) return null;
|
|
3624
|
-
const fieldCount = Object.keys(result).length;
|
|
3625
|
-
const status = normalizeCompletionStatus(result.status);
|
|
3626
|
-
const explicitlyFailed = status.startsWith('fail') || status === 'error';
|
|
3627
|
-
if (fieldCount < 2 && !explicitlyFailed) return null;
|
|
3628
|
-
return result;
|
|
3629
|
-
}
|
|
3630
|
-
|
|
3631
3523
|
function parseCompletionReportFile(dispatchItem, opts = {}) {
|
|
3632
3524
|
const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem?.id);
|
|
3633
3525
|
if (!reportPath || !fs.existsSync(reportPath)) {
|
|
@@ -3820,29 +3712,6 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
|
|
|
3820
3712
|
return { artifacts, notes, agentId, dispatchId };
|
|
3821
3713
|
}
|
|
3822
3714
|
|
|
3823
|
-
function persistCompletionReport(dispatchItem, completion, source = 'fallback') {
|
|
3824
|
-
if (!dispatchItem?.id || !completion || typeof completion !== 'object') return completion;
|
|
3825
|
-
const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem.id);
|
|
3826
|
-
if (!reportPath) return completion;
|
|
3827
|
-
const report = {
|
|
3828
|
-
...completion,
|
|
3829
|
-
status: completion.status || completion.outcome || 'unknown',
|
|
3830
|
-
_source: source,
|
|
3831
|
-
_path: reportPath,
|
|
3832
|
-
dispatchId: dispatchItem.id,
|
|
3833
|
-
agent: dispatchItem.agent || null,
|
|
3834
|
-
type: dispatchItem.type || null,
|
|
3835
|
-
completedAt: ts(),
|
|
3836
|
-
};
|
|
3837
|
-
try {
|
|
3838
|
-
safeWrite(reportPath, report);
|
|
3839
|
-
log('info', `Persisted ${source} completion report for ${dispatchItem.id}: ${reportPath}`);
|
|
3840
|
-
} catch (err) {
|
|
3841
|
-
log('warn', `Persist fallback completion report for ${dispatchItem.id}: ${err.message}`);
|
|
3842
|
-
}
|
|
3843
|
-
return report;
|
|
3844
|
-
}
|
|
3845
|
-
|
|
3846
3715
|
function normalizeCompletionStatus(status) {
|
|
3847
3716
|
return String(status || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
3848
3717
|
}
|
|
@@ -4487,6 +4356,12 @@ function handleHarnessIterationResult(stdout, structuredCompletion, meta, config
|
|
|
4487
4356
|
* - Returns silently when the PR is no longer active, or when its current
|
|
4488
4357
|
* reviewStatus is not in {changes-requested, waiting}. An already-approved
|
|
4489
4358
|
* PR doesn't need a re-review.
|
|
4359
|
+
* - P-e8b1c4d2: Returns silently when engine.autoReReviewPrs === false. This
|
|
4360
|
+
* mirrors the discovery-driven gate in engine.js:discoverFromPrs so the
|
|
4361
|
+
* toggle is a single source of truth across BOTH re-review paths
|
|
4362
|
+
* (open-loop / closure-loop). Without this gate the dashboard switch only
|
|
4363
|
+
* muted the discovery path, leaving the closure-loop free to keep
|
|
4364
|
+
* dispatching re-reviews — defeating the operator's intent.
|
|
4490
4365
|
* - Idempotent: getPrDispatchDedupeKey + addToDispatch dedup already skips a
|
|
4491
4366
|
* second review dispatch for the same (PR, type). No second WI ever lands.
|
|
4492
4367
|
* - Soft agent preference: agents whose charter advertises code-review or
|
|
@@ -4504,6 +4379,14 @@ function dispatchReReviewForFix(fixDispatchItem, meta, config) {
|
|
|
4504
4379
|
if (!addressesReviewWi) return null;
|
|
4505
4380
|
const pr = meta?.pr;
|
|
4506
4381
|
if (!pr?.id) return null;
|
|
4382
|
+
// P-e8b1c4d2 — honor the autoReReviewPrs kill-switch in the closure-loop too.
|
|
4383
|
+
// The open-loop discovery path in engine.js gates on the same flag; without
|
|
4384
|
+
// this guard the closure-loop silently bypassed the operator's toggle.
|
|
4385
|
+
const autoReReviewPrs = config?.engine?.autoReReviewPrs ?? shared.ENGINE_DEFAULTS.autoReReviewPrs;
|
|
4386
|
+
if (autoReReviewPrs === false) {
|
|
4387
|
+
log('info', `Re-review skipped for ${pr.id}: engine.autoReReviewPrs is off (closure-loop kill-switch)`);
|
|
4388
|
+
return null;
|
|
4389
|
+
}
|
|
4507
4390
|
// Re-read live PR state so we don't queue a re-review against an already
|
|
4508
4391
|
// merged/abandoned PR. The post-completion fix-update may have just flipped
|
|
4509
4392
|
// reviewStatus to 'waiting'; consult that fresh state via the central read.
|
|
@@ -4672,33 +4555,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
4672
4555
|
reportCompletion = null;
|
|
4673
4556
|
}
|
|
4674
4557
|
|
|
4675
|
-
const
|
|
4676
|
-
const summaryCompletion = (nonceMismatch || reportCompletion || fencedCompletion) ? null : parseCompletionFieldSummary(resultSummary);
|
|
4677
|
-
const fallbackCompletion = fencedCompletion || summaryCompletion;
|
|
4678
|
-
const fallbackSource = fencedCompletion && hasCompletionFence(stdout, runtimeName) ? 'fenced-completion' : 'summary-completion';
|
|
4679
|
-
// P-c8f5e1b3 — telemetry: completion-fallback. Emit a grep-able log + bump a
|
|
4680
|
-
// counter in metrics.json whenever the fenced or summary fallback fires (i.e.
|
|
4681
|
-
// the documented sidecar contract was missing). Used to confirm zero adoption
|
|
4682
|
-
// gaps before removing parseStructuredCompletion / parseCompletionFieldSummary
|
|
4683
|
-
// in a follow-up. Sidecar (happy) path is silent — no log, no increment.
|
|
4684
|
-
if (!reportCompletion && fallbackCompletion) {
|
|
4685
|
-
const counterKey = fallbackSource === 'fenced-completion' ? 'fenced' : 'summary';
|
|
4686
|
-
const wiId = dispatchItem.meta?.item?.id || null;
|
|
4687
|
-
log('info', `telemetry: completion-fallback agent=${agentId || 'unknown'} wi=${wiId || 'N/A'} runtime=${runtimeName} source=${fallbackSource}`);
|
|
4688
|
-
try {
|
|
4689
|
-
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
4690
|
-
mutateJsonFileLocked(metricsPath, (metrics) => {
|
|
4691
|
-
metrics = metrics || {};
|
|
4692
|
-
if (!metrics._engine) metrics._engine = {};
|
|
4693
|
-
if (!metrics._engine.completionFallbacks) metrics._engine.completionFallbacks = { fenced: 0, summary: 0 };
|
|
4694
|
-
metrics._engine.completionFallbacks[counterKey] = (metrics._engine.completionFallbacks[counterKey] || 0) + 1;
|
|
4695
|
-
return metrics;
|
|
4696
|
-
});
|
|
4697
|
-
} catch (err) {
|
|
4698
|
-
log('warn', `telemetry: completion-fallback metrics write failed: ${err.message}`);
|
|
4699
|
-
}
|
|
4700
|
-
}
|
|
4701
|
-
const structuredCompletion = reportCompletion || persistCompletionReport(dispatchItem, fallbackCompletion, fallbackSource);
|
|
4558
|
+
const structuredCompletion = reportCompletion;
|
|
4702
4559
|
if (structuredCompletion) {
|
|
4703
4560
|
if (structuredCompletion.summary) resultSummary = String(structuredCompletion.summary);
|
|
4704
4561
|
log('info', `Structured completion from ${agentId}: status=${structuredCompletion.status}, pr=${structuredCompletion.pr || 'N/A'}${structuredCompletion._source ? ` (${structuredCompletion._source})` : ''}`);
|
|
@@ -5638,8 +5495,6 @@ module.exports = {
|
|
|
5638
5495
|
parseAgentOutput,
|
|
5639
5496
|
parseReviewVerdict,
|
|
5640
5497
|
isReviewBailout,
|
|
5641
|
-
parseStructuredCompletion,
|
|
5642
|
-
parseCompletionFieldSummary,
|
|
5643
5498
|
parseCompletionNoop,
|
|
5644
5499
|
detectNonTerminalResultSummary,
|
|
5645
5500
|
deferNonTerminalCompletion,
|
|
@@ -5651,7 +5506,6 @@ module.exports = {
|
|
|
5651
5506
|
completionArtifactToNoteEntry,
|
|
5652
5507
|
mergeArtifactNotes,
|
|
5653
5508
|
promoteCompletionArtifacts,
|
|
5654
|
-
persistCompletionReport,
|
|
5655
5509
|
runPostCompletionHooks,
|
|
5656
5510
|
syncPrdFromPrs,
|
|
5657
5511
|
resolveWorkItemPath,
|
package/engine/meeting.js
CHANGED
|
@@ -916,6 +916,36 @@ function checkMeetingTimeouts(config) {
|
|
|
916
916
|
const elapsed = Date.now() - roundStartedMs;
|
|
917
917
|
if (elapsed < timeout) continue;
|
|
918
918
|
|
|
919
|
+
// BUG-H11: when a hard timeout will fire, kill in-flight meeting
|
|
920
|
+
// dispatches BEFORE entering the meeting lock so they stop consuming
|
|
921
|
+
// worker slots / LLM tokens immediately rather than draining over the
|
|
922
|
+
// next tens of minutes. _killMeetingDispatches takes the dispatch.json
|
|
923
|
+
// lock and shells out to kill processes — per CLAUDE.md and the
|
|
924
|
+
// advanceMeetingRound/endMeeting/deleteMeeting pattern at L834, that
|
|
925
|
+
// MUST run outside the meeting lock. We pre-evaluate eligibility from
|
|
926
|
+
// the snapshot using the same predicate the mutate callback below
|
|
927
|
+
// re-checks under lock; if the timeout has already advanced/cleared,
|
|
928
|
+
// the kill is a no-op on dispatch.json (no matching meetingId entries).
|
|
929
|
+
const snapshotRoundName = snapshot.status === 'investigating'
|
|
930
|
+
? 'investigate'
|
|
931
|
+
: snapshot.status === 'debating'
|
|
932
|
+
? 'debate'
|
|
933
|
+
: snapshot.status === 'concluding'
|
|
934
|
+
? 'conclude'
|
|
935
|
+
: null;
|
|
936
|
+
const snapshotHardTimeoutWillFire = elapsed >= hardTimeout && (
|
|
937
|
+
// Round-timeout branch: non-conclude round + not all participants done
|
|
938
|
+
(
|
|
939
|
+
(snapshotRoundName === 'investigate' || snapshotRoundName === 'debate')
|
|
940
|
+
&& !allParticipantsFinishedRound(snapshot, snapshotRoundName, snapshot.round)
|
|
941
|
+
)
|
|
942
|
+
// Conclusion-timeout branch: concluding status
|
|
943
|
+
|| snapshot.status === 'concluding'
|
|
944
|
+
);
|
|
945
|
+
if (snapshotHardTimeoutWillFire) {
|
|
946
|
+
_killMeetingDispatches(snapshot.id);
|
|
947
|
+
}
|
|
948
|
+
|
|
919
949
|
// Re-evaluate the timeout transition under the file lock to avoid lost
|
|
920
950
|
// updates if an agent finalised mid-tick. Helpers (advanceMeetingIfRoundComplete
|
|
921
951
|
// etc.) operate on the locked-and-rehydrated meeting object.
|
package/engine/playbook.js
CHANGED
|
@@ -298,6 +298,11 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
|
298
298
|
'existing_prd_json', // only set when re-running plan-to-prd over an existing PRD
|
|
299
299
|
'branch_strategy_hint', // only set for shared-branch plans
|
|
300
300
|
'review_note', // only set on fix/review tasks tied to a comment
|
|
301
|
+
// P-4d6e2af3 — comma-separated list of target projects for cross-repo
|
|
302
|
+
// plans. Only set in engine.js's WORK_TYPE.PLAN branch when the WI carries
|
|
303
|
+
// item._targetProjects (≥2 entries from dashboard.js#buildPlanWorkItem).
|
|
304
|
+
// Single-project plans legitimately resolve to ''.
|
|
305
|
+
'target_projects',
|
|
301
306
|
// PR-context vars on non-PR tasks (implement/explore/etc.)
|
|
302
307
|
'pr_id', 'pr_number', 'pr_title', 'pr_branch', 'pr_author', 'pr_url',
|
|
303
308
|
'reviewer',
|
|
@@ -868,6 +873,16 @@ function renderPlaybook(type, vars) {
|
|
|
868
873
|
return (val && String(val).trim()) ? block : '';
|
|
869
874
|
});
|
|
870
875
|
|
|
876
|
+
// P-c1f87a92 — negative conditional blocks: {{^key}}...{{/key}} — include
|
|
877
|
+
// block only if key is FALSY / absent / whitespace-only. Mirror the
|
|
878
|
+
// positive-block trim() check above so single-value semantics line up
|
|
879
|
+
// (vars.target_projects = '' or undefined renders the {{^}} branch, the
|
|
880
|
+
// same way it omits the {{#}} branch).
|
|
881
|
+
content = content.replace(/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, block) => {
|
|
882
|
+
const val = allVars[key];
|
|
883
|
+
return (val && String(val).trim()) ? '' : block;
|
|
884
|
+
});
|
|
885
|
+
|
|
871
886
|
// Substitute variables
|
|
872
887
|
for (const [key, val] of Object.entries(allVars)) {
|
|
873
888
|
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(val));
|
package/engine/queries.js
CHANGED
|
@@ -1521,6 +1521,21 @@ function _getPrdInputHash(projects) {
|
|
|
1521
1521
|
}
|
|
1522
1522
|
// Static pr-links.json overrides (affect shared.getPrLinks(); missing project mtimes otherwise)
|
|
1523
1523
|
try { mtimes.push(fs.statSync(path.join(MINIONS_DIR, 'engine', 'pr-links.json')).mtimeMs); } catch { mtimes.push(0); }
|
|
1524
|
+
// W-mqacrzis0003df4a — Source-plan markdown mtimes. Without this, the
|
|
1525
|
+
// per-PRD planStale fresh-computation at lines ~1568-1576 is never reached
|
|
1526
|
+
// after the user revises plans/<x>.md until the engine tick (~10s) flips
|
|
1527
|
+
// the persisted plan.planStale flag. The cached `_prdResultCache` hits the
|
|
1528
|
+
// early-return and serves the prior (stale) `planStale: false` to readers
|
|
1529
|
+
// long enough for a fast user to Approve and silently bypass diff-aware
|
|
1530
|
+
// regen. Push the PLANS_DIR directory mtime first (catches add/delete on
|
|
1531
|
+
// POSIX) then every .md file's mtime in sorted order for determinism.
|
|
1532
|
+
try { mtimes.push(fs.statSync(PLANS_DIR).mtimeMs); } catch { mtimes.push(0); }
|
|
1533
|
+
try {
|
|
1534
|
+
const planFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md')).sort();
|
|
1535
|
+
for (const f of planFiles) {
|
|
1536
|
+
try { mtimes.push(fs.statSync(path.join(PLANS_DIR, f)).mtimeMs); } catch { mtimes.push(0); }
|
|
1537
|
+
}
|
|
1538
|
+
} catch { /* plans/ may not exist in some test/init states */ }
|
|
1524
1539
|
return { hash: mtimes.join(','), prdDirMtime, archiveDirMtime };
|
|
1525
1540
|
}
|
|
1526
1541
|
|
|
@@ -1770,7 +1785,17 @@ function getPrdInfo(config) {
|
|
|
1770
1785
|
items: items.map(i => ({
|
|
1771
1786
|
id: i.id, name: i.name || i.title, priority: i.priority,
|
|
1772
1787
|
complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
|
|
1773
|
-
description: i.description || '',
|
|
1788
|
+
description: i.description || '',
|
|
1789
|
+
// P-e8d49105 — projects[] feeds the per-item badge in
|
|
1790
|
+
// dashboard/js/render-prd.js:250-252 + the plan-group rollup at :243.
|
|
1791
|
+
// Honor a pre-populated projects[] (forward-compat for any future
|
|
1792
|
+
// multi-repo single item); otherwise derive a single-element array
|
|
1793
|
+
// from i.project so cross-repo PRDs (every item.project may differ)
|
|
1794
|
+
// light up the dormant badge code without changing single-project
|
|
1795
|
+
// behavior — i.project keeps being emitted unchanged below.
|
|
1796
|
+
projects: (Array.isArray(i.projects) && i.projects.length > 0)
|
|
1797
|
+
? i.projects
|
|
1798
|
+
: (i.project ? [i.project] : []),
|
|
1774
1799
|
prs: prdToPr[i.id] || [], depends_on: i.depends_on || [],
|
|
1775
1800
|
project: i.project || '', source: i._source || '', planSummary: i._planSummary || '', planProject: i._planProject || '', planStatus: i._planStatus || 'active', _archived: i._archived || false, sourcePlan: i._sourcePlan || '',
|
|
1776
1801
|
branchStrategy: i._branchStrategy || 'parallel',
|
|
@@ -444,6 +444,7 @@ function buildArgs(opts = {}) {
|
|
|
444
444
|
disableBuiltinMcps,
|
|
445
445
|
suppressAgentsMd,
|
|
446
446
|
reasoningSummaries,
|
|
447
|
+
disabledMcpServers,
|
|
447
448
|
} = opts;
|
|
448
449
|
|
|
449
450
|
const args = [
|
|
@@ -478,6 +479,16 @@ function buildArgs(opts = {}) {
|
|
|
478
479
|
if (suppressAgentsMd === true) args.push('--no-custom-instructions');
|
|
479
480
|
if (reasoningSummaries === true) args.push('--enable-reasoning-summaries');
|
|
480
481
|
|
|
482
|
+
// P-mcp-storm — Disable named user-scope MCP servers per spawn. Copilot loads
|
|
483
|
+
// ~/.copilot/mcp-config.json unconditionally (not gated by --add-dir/cwd), so
|
|
484
|
+
// this --disable-mcp-server <name> (repeatable) is the only lever to keep an
|
|
485
|
+
// operator's interactive MCP servers out of autonomous agent dispatches.
|
|
486
|
+
if (Array.isArray(disabledMcpServers)) {
|
|
487
|
+
for (const name of disabledMcpServers) {
|
|
488
|
+
if (name) args.push('--disable-mcp-server', String(name));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
481
492
|
// --stream takes a value: 'on' or 'off'. Caller passes that exact value.
|
|
482
493
|
if (stream === 'on' || stream === 'off') {
|
|
483
494
|
args.push('--stream', stream);
|
|
@@ -504,6 +515,14 @@ function buildSpawnFlags(opts = {}) {
|
|
|
504
515
|
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
505
516
|
if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
|
|
506
517
|
if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
|
|
518
|
+
// P-mcp-storm — forward each disabled user MCP server so spawn-agent.js can
|
|
519
|
+
// re-emit it on the copilot CLI (parseSpawnArgs accumulates these into
|
|
520
|
+
// opts.disabledMcpServers, which buildArgs turns back into --disable-mcp-server).
|
|
521
|
+
if (Array.isArray(opts.disabledMcpServers)) {
|
|
522
|
+
for (const name of opts.disabledMcpServers) {
|
|
523
|
+
if (name) flags.push('--disable-mcp-server', String(name));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
507
526
|
return flags;
|
|
508
527
|
}
|
|
509
528
|
|