@yemi33/minions 0.1.2380 → 0.1.2382
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 +41 -2
- package/dashboard/js/refresh.js +9 -3
- package/dashboard/js/render-other.js +81 -23
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +47 -25
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +126 -62
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/engine-restart.md +10 -0
- package/docs/live-checkout-mode.md +45 -26
- package/docs/worktree-lifecycle.md +9 -0
- package/engine/claude-md-context.js +195 -0
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +78 -24
- package/engine/lifecycle.js +129 -84
- package/engine/live-checkout.js +193 -149
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +47 -15
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +6 -0
- package/engine/runtimes/codex.js +18 -0
- package/engine/runtimes/copilot.js +7 -0
- package/engine/shared.js +199 -106
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +301 -31
- package/package.json +1 -1
- package/prompts/cc-system.md +7 -7
|
@@ -838,6 +838,12 @@ const capabilities = {
|
|
|
838
838
|
budgetCap: true,
|
|
839
839
|
// Supports `--bare` (suppress CLAUDE.md auto-discovery)
|
|
840
840
|
bareMode: true,
|
|
841
|
+
// The Claude Code CLI reads repo-authored CLAUDE.md files itself at startup
|
|
842
|
+
// (unless `--bare`). TRUE here tells the engine's CLAUDE.md-propagation layer
|
|
843
|
+
// (engine/claude-md-context.js, injected in engine/playbook.js) to SKIP this
|
|
844
|
+
// runtime — re-injecting CLAUDE.md content into the prompt would double it.
|
|
845
|
+
// Copilot/Codex set this false because they have no native CLAUDE.md auto-load.
|
|
846
|
+
claudeMdNativeDiscovery: true,
|
|
841
847
|
// Supports `--fallback-model <id>`
|
|
842
848
|
fallbackModel: true,
|
|
843
849
|
// Engine controls session persistence (writes session.json on completion)
|
package/engine/runtimes/codex.js
CHANGED
|
@@ -840,6 +840,20 @@ function getModelCacheKey({ env = process.env, timeoutMs = 10000 } = {}) {
|
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
function getModelCacheKey({ env = process.env, timeoutMs = 10000 } = {}) {
|
|
844
|
+
const resolved = resolveBinary({ env });
|
|
845
|
+
if (!resolved) return null;
|
|
846
|
+
try {
|
|
847
|
+
return _execFileCapture(resolved.bin, [...(resolved.leadingArgs || []), '--version'], {
|
|
848
|
+
env,
|
|
849
|
+
timeoutMs,
|
|
850
|
+
native: resolved.native,
|
|
851
|
+
}).trim() || null;
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
843
857
|
function createStreamConsumer(ctx) {
|
|
844
858
|
let deltaText = '';
|
|
845
859
|
let terminalText = '';
|
|
@@ -901,6 +915,10 @@ const capabilities = {
|
|
|
901
915
|
budgetCap: false,
|
|
902
916
|
bareMode: false,
|
|
903
917
|
fallbackModel: false,
|
|
918
|
+
// Codex has no native CLAUDE.md auto-load — FALSE opts it into the engine's
|
|
919
|
+
// CLAUDE.md-propagation layer (engine/claude-md-context.js) so repo-authored
|
|
920
|
+
// CLAUDE.md guidance reaches the agent prompt.
|
|
921
|
+
claudeMdNativeDiscovery: false,
|
|
904
922
|
sessionPersistenceControl: false,
|
|
905
923
|
resumePromptCarryover: true,
|
|
906
924
|
streamConsumer: true,
|
|
@@ -1273,6 +1273,13 @@ const capabilities = {
|
|
|
1273
1273
|
budgetCap: false,
|
|
1274
1274
|
// No --bare equivalent.
|
|
1275
1275
|
bareMode: false,
|
|
1276
|
+
// Copilot has NO native CLAUDE.md auto-load — it only reads its own AGENTS.md
|
|
1277
|
+
// (native custom-instruction discovery owned by the CLI itself). FALSE
|
|
1278
|
+
// here opts this runtime INTO the engine's CLAUDE.md-propagation layer
|
|
1279
|
+
// (engine/claude-md-context.js) so repo-authored CLAUDE.md guidance is
|
|
1280
|
+
// surfaced in the prompt. This is orthogonal to AGENTS.md discovery —
|
|
1281
|
+
// CLAUDE.md carries no split-brain risk because Copilot never reads it itself.
|
|
1282
|
+
claudeMdNativeDiscovery: false,
|
|
1276
1283
|
// No --fallback-model
|
|
1277
1284
|
fallbackModel: false,
|
|
1278
1285
|
// Copilot manages session state internally in ~/.copilot/session-state/
|
package/engine/shared.js
CHANGED
|
@@ -1037,8 +1037,16 @@ function validateDispatchTmpDir(dirPath) {
|
|
|
1037
1037
|
|
|
1038
1038
|
function removeDispatchTmpDir(dirPath) {
|
|
1039
1039
|
if (!validateDispatchTmpDir(dirPath)) return false;
|
|
1040
|
-
try {
|
|
1041
|
-
|
|
1040
|
+
try {
|
|
1041
|
+
_retryFsOp(
|
|
1042
|
+
() => fs.rmSync(dirPath, { recursive: true, force: true }),
|
|
1043
|
+
`removeDispatchTmpDir(${dirPath})`
|
|
1044
|
+
);
|
|
1045
|
+
return true;
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
log('warn', `removeDispatchTmpDir failed after retries: ${e.message}`, { path: dirPath, error: e.code });
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1042
1050
|
}
|
|
1043
1051
|
|
|
1044
1052
|
// Read a file using O_NOFOLLOW where the platform supports it. On Windows
|
|
@@ -1439,48 +1447,37 @@ function withFileLock(lockPath, fn, {
|
|
|
1439
1447
|
const isLockRaceErr = err.code === 'EEXIST' ||
|
|
1440
1448
|
(process.platform === 'win32' && err.code === 'EPERM');
|
|
1441
1449
|
if (!isLockRaceErr) throw err;
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
// - dead PID → reap.
|
|
1446
|
-
// - alive PID, mtime <= 5× → don't reap (legitimate slow holder).
|
|
1447
|
-
// - alive PID, mtime > 5× → reap (last-resort guard against
|
|
1448
|
-
// stuck holders that never released).
|
|
1449
|
-
// 3. Legacy / empty / non-JSON lockfile → mtime-only path (reap).
|
|
1450
|
+
// PID-owned locks are safe to reap as soon as their owner is dead.
|
|
1451
|
+
// Alive owners retain the 5× stale window; legacy/partial lockfiles
|
|
1452
|
+
// without a readable PID retain the mtime-only stale window.
|
|
1450
1453
|
try {
|
|
1451
1454
|
const stat = fs.statSync(lockPath);
|
|
1452
1455
|
const mtimeAge = Date.now() - stat.mtimeMs;
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
holderPid = parsed.pid;
|
|
1460
|
-
}
|
|
1461
|
-
} catch { /* legacy/empty/corrupt lock → fall through to mtime-only */ }
|
|
1462
|
-
|
|
1463
|
-
let shouldReap;
|
|
1464
|
-
if (holderPid !== null) {
|
|
1465
|
-
shouldReap = !processUtils.isPidAlive(holderPid) || mtimeAge > LOCK_STALE_MS * 5;
|
|
1466
|
-
} else {
|
|
1467
|
-
// Legacy empty/non-JSON lockfile: trust mtime alone
|
|
1468
|
-
shouldReap = true;
|
|
1456
|
+
let holderPid = null;
|
|
1457
|
+
try {
|
|
1458
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
1459
|
+
const parsed = JSON.parse(raw);
|
|
1460
|
+
if (parsed && Number.isFinite(parsed.pid) && parsed.pid > 0) {
|
|
1461
|
+
holderPid = parsed.pid;
|
|
1469
1462
|
}
|
|
1463
|
+
} catch { /* legacy/empty/corrupt lock → mtime-only fallback */ }
|
|
1470
1464
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1465
|
+
const shouldReap = holderPid !== null
|
|
1466
|
+
? !processUtils.isPidAlive(holderPid) || mtimeAge > LOCK_STALE_MS * 5
|
|
1467
|
+
: mtimeAge > LOCK_STALE_MS;
|
|
1468
|
+
|
|
1469
|
+
if (shouldReap) {
|
|
1470
|
+
try {
|
|
1471
|
+
fs.unlinkSync(lockPath);
|
|
1472
|
+
} catch (unlinkErr) {
|
|
1473
|
+
// ENOENT: another process deleted the lock between stat and unlink — safe to retry.
|
|
1474
|
+
// EPERM on Windows: file is in 'pending delete' state from a concurrent
|
|
1475
|
+
// reaper — the OS will finalize the delete shortly; retry will succeed.
|
|
1476
|
+
const tolerable = unlinkErr.code === 'ENOENT' ||
|
|
1477
|
+
(process.platform === 'win32' && unlinkErr.code === 'EPERM');
|
|
1478
|
+
if (!tolerable) throw unlinkErr;
|
|
1483
1479
|
}
|
|
1480
|
+
continue; // lock just removed — retry immediately
|
|
1484
1481
|
}
|
|
1485
1482
|
} catch (staleErr) {
|
|
1486
1483
|
// ENOENT from statSync: lock file disappeared between EEXIST and stat — retry will succeed.
|
|
@@ -2481,6 +2478,80 @@ function classifyInboxItem(name, content) {
|
|
|
2481
2478
|
// config.json keep working without an on-disk rewrite. Tracked in
|
|
2482
2479
|
// docs/deprecated.json (id: worktreemode-field-rename).
|
|
2483
2480
|
const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
2481
|
+
const LEGACY_LIVE_VALIDATION_TYPE_ALIASES = Object.freeze({
|
|
2482
|
+
'build-and-test': 'test',
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
function normalizeLiveValidationWorkType(value) {
|
|
2486
|
+
if (typeof value !== 'string') return '';
|
|
2487
|
+
const type = value.trim();
|
|
2488
|
+
return LEGACY_LIVE_VALIDATION_TYPE_ALIASES[type] || type;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
function inspectLiveValidation(value) {
|
|
2492
|
+
if (value === undefined || value === null || value === '') {
|
|
2493
|
+
return { configured: false, types: [], normalizedType: undefined, error: null };
|
|
2494
|
+
}
|
|
2495
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
2496
|
+
return {
|
|
2497
|
+
configured: true,
|
|
2498
|
+
types: [],
|
|
2499
|
+
normalizedType: undefined,
|
|
2500
|
+
error: 'Invalid liveValidation: must be an object { type, autoDispatch }.',
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
const rawType = value.type;
|
|
2505
|
+
const rawTypes = Array.isArray(rawType) ? rawType : [rawType];
|
|
2506
|
+
if (
|
|
2507
|
+
(typeof rawType !== 'string' && !Array.isArray(rawType))
|
|
2508
|
+
|| rawTypes.length === 0
|
|
2509
|
+
|| !rawTypes.every(type => typeof type === 'string' && type.trim())
|
|
2510
|
+
) {
|
|
2511
|
+
const accepted = [...LIVE_VALIDATION_WORK_TYPES].sort().join(', ');
|
|
2512
|
+
return {
|
|
2513
|
+
configured: true,
|
|
2514
|
+
types: [],
|
|
2515
|
+
normalizedType: undefined,
|
|
2516
|
+
error: `Invalid liveValidation: "type" is required and must be a non-empty string, or a non-empty array of live-capable work-item types. Accepted values: ${accepted}.`,
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
const normalizedTypes = [...new Set(rawTypes.map(normalizeLiveValidationWorkType))];
|
|
2521
|
+
const types = normalizedTypes.filter(type => LIVE_VALIDATION_WORK_TYPES.has(type));
|
|
2522
|
+
const invalidTypes = normalizedTypes.filter(type => !LIVE_VALIDATION_WORK_TYPES.has(type));
|
|
2523
|
+
let error = null;
|
|
2524
|
+
if (invalidTypes.length > 0) {
|
|
2525
|
+
const accepted = [...LIVE_VALIDATION_WORK_TYPES].sort().join(', ');
|
|
2526
|
+
error = `Invalid liveValidation type(s): ${invalidTypes.join(', ')}. Accepted values: ${accepted}.`;
|
|
2527
|
+
}
|
|
2528
|
+
if (!error && Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
2529
|
+
if (typeof value.autoDispatch !== 'boolean') {
|
|
2530
|
+
error = 'Invalid liveValidation: "autoDispatch" must be a boolean.';
|
|
2531
|
+
} else if (value.autoDispatch && !normalizedTypes.includes(WORK_TYPE.TEST)) {
|
|
2532
|
+
error = 'Invalid liveValidation: autoDispatch requires "test" in liveValidation.type so the generated validation work item runs on the live checkout.';
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return {
|
|
2536
|
+
configured: true,
|
|
2537
|
+
types,
|
|
2538
|
+
normalizedType: Array.isArray(rawType) ? normalizedTypes : normalizedTypes[0],
|
|
2539
|
+
error,
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Return the canonical, live-capable work-item types configured for a hybrid
|
|
2544
|
+
// project. Raw config is read defensively because config.json can predate the
|
|
2545
|
+
// validator; invalid entries are ignored rather than turning hybrid into full
|
|
2546
|
+
// live mode. `build-and-test` is retained as a read-compat alias for `test`.
|
|
2547
|
+
function resolveLiveValidationTypes(project) {
|
|
2548
|
+
return inspectLiveValidation(project && project.liveValidation).types;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function isLiveValidationConfigInvalid(project) {
|
|
2552
|
+
const inspection = inspectLiveValidation(project && project.liveValidation);
|
|
2553
|
+
return inspection.configured && !!inspection.error;
|
|
2554
|
+
}
|
|
2484
2555
|
|
|
2485
2556
|
// Resolve a project's effective checkout mode, honoring the legacy
|
|
2486
2557
|
// `worktreeMode` field for back-compat. Reading order:
|
|
@@ -2492,37 +2563,37 @@ const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
|
2492
2563
|
//
|
|
2493
2564
|
// Optional second argument `workItemType` enables liveValidation routing:
|
|
2494
2565
|
// If checkoutMode === 'live' AND project.liveValidation is set:
|
|
2495
|
-
// - workItemType matches liveValidation.type → 'live'
|
|
2496
|
-
// - otherwise → 'worktree'
|
|
2566
|
+
// - workItemType matches liveValidation.type → 'live'
|
|
2567
|
+
// - otherwise → 'worktree'
|
|
2497
2568
|
// liveValidation without checkoutMode: 'live' is ignored with a warn log.
|
|
2498
2569
|
function resolveCheckoutMode(project, workItemType) {
|
|
2499
2570
|
if (!project || typeof project !== 'object') return CHECKOUT_MODES.WORKTREE;
|
|
2500
2571
|
const canonical = project.checkoutMode;
|
|
2572
|
+
const legacy = project.worktreeMode;
|
|
2573
|
+
let effectiveMode = CHECKOUT_MODES.WORKTREE;
|
|
2574
|
+
if (canonical === CHECKOUT_MODES.LIVE || canonical === CHECKOUT_MODES.WORKTREE) {
|
|
2575
|
+
effectiveMode = canonical;
|
|
2576
|
+
} else if (legacy === 'live') {
|
|
2577
|
+
effectiveMode = CHECKOUT_MODES.LIVE;
|
|
2578
|
+
}
|
|
2501
2579
|
// liveValidation without checkoutMode: 'live' is a misconfiguration — warn and ignore.
|
|
2502
|
-
if (project.liveValidation &&
|
|
2580
|
+
if (project.liveValidation && effectiveMode !== CHECKOUT_MODES.LIVE) {
|
|
2503
2581
|
log('warn', 'resolveCheckoutMode: liveValidation is set but checkoutMode is not "live" — liveValidation ignored',
|
|
2504
2582
|
{ projectName: project.name });
|
|
2505
2583
|
}
|
|
2506
|
-
if (
|
|
2584
|
+
if (effectiveMode === CHECKOUT_MODES.LIVE) {
|
|
2507
2585
|
// Apply liveValidation routing when the block is present and workItemType is provided.
|
|
2508
2586
|
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
2509
2587
|
// array of strings (W-mr2m1ute000a9c01, multi-select hybrid types) —
|
|
2510
2588
|
// normalize to an array and do a membership check so either shape works.
|
|
2511
2589
|
if (project.liveValidation && workItemType !== undefined) {
|
|
2512
|
-
const lvTypes =
|
|
2513
|
-
|
|
2514
|
-
: [project.liveValidation.type];
|
|
2515
|
-
return lvTypes.includes(workItemType)
|
|
2590
|
+
const lvTypes = resolveLiveValidationTypes(project);
|
|
2591
|
+
return lvTypes.includes(normalizeLiveValidationWorkType(workItemType))
|
|
2516
2592
|
? CHECKOUT_MODES.LIVE
|
|
2517
2593
|
: CHECKOUT_MODES.WORKTREE;
|
|
2518
2594
|
}
|
|
2519
2595
|
return CHECKOUT_MODES.LIVE;
|
|
2520
2596
|
}
|
|
2521
|
-
if (canonical === CHECKOUT_MODES.WORKTREE) return CHECKOUT_MODES.WORKTREE;
|
|
2522
|
-
// Legacy field fallback (only consulted when checkoutMode is absent/unknown).
|
|
2523
|
-
const legacy = project.worktreeMode;
|
|
2524
|
-
if (legacy === 'live') return CHECKOUT_MODES.LIVE;
|
|
2525
|
-
// legacy 'isolated' (and anything else) → the default worktree behavior.
|
|
2526
2597
|
return CHECKOUT_MODES.WORKTREE;
|
|
2527
2598
|
}
|
|
2528
2599
|
|
|
@@ -2538,9 +2609,10 @@ function isLiveCheckoutProject(project) {
|
|
|
2538
2609
|
// anything else (undefined / null / string) is treated as unset so the next
|
|
2539
2610
|
// tier (or the `false` default) decides. When ON and a live-checkout tree is
|
|
2540
2611
|
// dirty, `engine/live-checkout.js#prepareLiveCheckout` runs
|
|
2541
|
-
// `git
|
|
2542
|
-
//
|
|
2543
|
-
// so it is opt-in
|
|
2612
|
+
// `git reset --hard HEAD` + `git clean -fd` instead of refusing with
|
|
2613
|
+
// LIVE_CHECKOUT_DIRTY. This discards uncommitted tracked and untracked work but
|
|
2614
|
+
// preserves the current branch and committed history, so it is opt-in. Sibling
|
|
2615
|
+
// of `resolveCheckoutMode` — pure, no I/O.
|
|
2544
2616
|
function resolveLiveCheckoutAutoReset(project, engine) {
|
|
2545
2617
|
if (project && typeof project === 'object' && typeof project.liveCheckoutAutoReset === 'boolean') {
|
|
2546
2618
|
return project.liveCheckoutAutoReset;
|
|
@@ -2605,16 +2677,14 @@ function validateCheckoutMode(value) {
|
|
|
2605
2677
|
}
|
|
2606
2678
|
|
|
2607
2679
|
// Validate + normalize a per-project `liveValidation` block (hybrid mode).
|
|
2608
|
-
// Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }:
|
|
2609
|
-
// work items
|
|
2610
|
-
//
|
|
2611
|
-
// checkout. See resolveCheckoutMode (which routes per work-item type) and
|
|
2612
|
-
// docs/live-checkout-mode.md.
|
|
2680
|
+
// Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }: only
|
|
2681
|
+
// work items whose type is IN liveValidation.type run in-place; all other types
|
|
2682
|
+
// use isolated worktrees. See resolveCheckoutMode and docs/live-checkout-mode.md.
|
|
2613
2683
|
//
|
|
2614
2684
|
// `type` may be a single work-item-type string (legacy / back-compat) OR a
|
|
2615
2685
|
// non-empty array of non-empty-string work-item types
|
|
2616
2686
|
// (W-mr2m1ute000a9c01 — multi-select hybrid types, e.g. both "implement" and
|
|
2617
|
-
// "fix"
|
|
2687
|
+
// "fix" run live while other types stay in worktrees). Every reader of
|
|
2618
2688
|
// liveValidation.type (resolveCheckoutMode, lifecycle.autoDispatchLiveValidationWi,
|
|
2619
2689
|
// dashboard project mapper / pill / CC label) must handle BOTH shapes —
|
|
2620
2690
|
// existing config.json entries with a plain string `type` continue to work
|
|
@@ -2623,10 +2693,8 @@ function validateCheckoutMode(value) {
|
|
|
2623
2693
|
// Returns:
|
|
2624
2694
|
// - undefined → caller should clear the field (empty / null / '' input)
|
|
2625
2695
|
// - { type, autoDispatch? } normalized object on success, where `type` is
|
|
2626
|
-
//
|
|
2627
|
-
//
|
|
2628
|
-
// into an array so already-persisted single-type configs round-trip
|
|
2629
|
-
// byte-identical through a Settings save that doesn't touch this field.
|
|
2696
|
+
// the caller's input shape (string stays a string; array stays an array),
|
|
2697
|
+
// with values trimmed, normalized, and deduplicated.
|
|
2630
2698
|
// Throws HTTP 400 (via _httpError) on malformed input, or when the effective
|
|
2631
2699
|
// checkout mode is not 'live' (liveValidation is meaningless without it — it is
|
|
2632
2700
|
// silently ignored at resolve time, so we refuse to persist a misconfiguration).
|
|
@@ -2636,45 +2704,15 @@ function validateCheckoutMode(value) {
|
|
|
2636
2704
|
// applied so the gate sees the intended final mode.
|
|
2637
2705
|
function validateLiveValidation(value, opts) {
|
|
2638
2706
|
if (value === undefined || value === null || value === '') return undefined;
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2707
|
+
const inspection = inspectLiveValidation(value);
|
|
2708
|
+
if (inspection.error) throw _httpError(400, inspection.error);
|
|
2642
2709
|
const checkoutMode = opts && opts.checkoutMode;
|
|
2643
2710
|
if (checkoutMode !== CHECKOUT_MODES.LIVE) {
|
|
2644
2711
|
throw _httpError(400, 'liveValidation requires checkoutMode "live" (hybrid mode). Set checkoutMode to "live" first, or clear liveValidation.');
|
|
2645
2712
|
}
|
|
2646
|
-
const
|
|
2647
|
-
const rawType = value.type;
|
|
2648
|
-
let out;
|
|
2649
|
-
if (typeof rawType === 'string') {
|
|
2650
|
-
const trimmed = rawType.trim();
|
|
2651
|
-
if (!trimmed) throw _httpError(400, typeErrorMsg);
|
|
2652
|
-
out = { type: trimmed };
|
|
2653
|
-
} else if (Array.isArray(rawType)) {
|
|
2654
|
-
if (!rawType.every(t => typeof t === 'string' && t.trim().length > 0)) {
|
|
2655
|
-
throw _httpError(400, typeErrorMsg);
|
|
2656
|
-
}
|
|
2657
|
-
// Dedupe (preserving first-seen order) and cap at the number of distinct
|
|
2658
|
-
// work-item types the engine recognizes — a longer array can only be
|
|
2659
|
-
// full of duplicates or invented types.
|
|
2660
|
-
const deduped = [...new Set(rawType.map(t => t.trim()))];
|
|
2661
|
-
if (deduped.length === 0) throw _httpError(400, typeErrorMsg);
|
|
2662
|
-
if (deduped.length > VALID_WORK_TYPES.size) {
|
|
2663
|
-
throw _httpError(400, `Invalid liveValidation: "type" array has ${deduped.length} entries, more than the ${VALID_WORK_TYPES.size} distinct work-item types the engine recognizes — remove duplicates or invalid types.`);
|
|
2664
|
-
}
|
|
2665
|
-
out = { type: deduped };
|
|
2666
|
-
} else {
|
|
2667
|
-
throw _httpError(400, typeErrorMsg);
|
|
2668
|
-
}
|
|
2669
|
-
// autoDispatch (optional): when true, lifecycle auto-creates a SINGLE
|
|
2670
|
-
// validation WI after each coding WI completes with a PR
|
|
2671
|
-
// (engine/lifecycle.js autoDispatchLiveValidationWi). Even when `type` is a
|
|
2672
|
-
// multi-entry array (which drives checkout ROUTING — the types that run live),
|
|
2673
|
-
// exactly one validation WI is filed, of the canonical `test` type when
|
|
2674
|
-
// configured else the first configured live type. Coerce to a strict
|
|
2675
|
-
// boolean; absent leaves it off so the operator opts in explicitly.
|
|
2713
|
+
const out = { type: inspection.normalizedType };
|
|
2676
2714
|
if (Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
2677
|
-
out.autoDispatch =
|
|
2715
|
+
out.autoDispatch = value.autoDispatch;
|
|
2678
2716
|
}
|
|
2679
2717
|
return out;
|
|
2680
2718
|
}
|
|
@@ -3079,9 +3117,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3079
3117
|
// description the validator will never accept (see stage-output
|
|
3080
3118
|
// substitution bug above) got re-evaluated forever, once per tick,
|
|
3081
3119
|
// invisible to the dashboard. Once the SAME (unchanged) description is
|
|
3082
|
-
// rejected this many consecutive times, the WI
|
|
3083
|
-
//
|
|
3084
|
-
// re-queued into review again.
|
|
3120
|
+
// rejected this many consecutive times, the WI stays pending but is marked
|
|
3121
|
+
// exhausted so discovery pauses until an operator edits it.
|
|
3085
3122
|
preDispatchEvalMaxRejections: 5,
|
|
3086
3123
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
3087
3124
|
autoApplyReviewVote: false, // Master gate for platform vote mutations; local verdicts remain informational when false.
|
|
@@ -3214,6 +3251,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3214
3251
|
maxMeetingPromptBytes: 16 * 1024, // cap meeting findings/debate context injected into prompts
|
|
3215
3252
|
maxMeetingHumanNotesBytes: 2 * 1024, // cap human note bullet lists injected into meeting prompts
|
|
3216
3253
|
maxPipelineMeetingContextBytes: 16 * 1024, // cap aggregated meeting/dependency context for pipeline plan generation
|
|
3254
|
+
maxPipelineStageHandoffBytes: 32 * 1024, // cap current-run note content exposed through {{stages.<id>.handoff}}
|
|
3217
3255
|
notesArchiveMaxFiles: 2000, // keep notes/archive bounded during periodic cleanup
|
|
3218
3256
|
// ── Worktree pool (W-mp73ya3e000me6c5) ─────────────────────────────────────
|
|
3219
3257
|
// Per-project warm pool: completed worktrees are parked detached at
|
|
@@ -3283,7 +3321,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3283
3321
|
'node', 'bun', 'npm', 'npx', 'pnpm', 'yarn',
|
|
3284
3322
|
'python', 'python3', 'pip', 'pip3',
|
|
3285
3323
|
'docker', 'podman',
|
|
3286
|
-
'adb', 'emulator',
|
|
3324
|
+
'adb', 'emulator', 'maestro',
|
|
3287
3325
|
'gradle', 'gradlew', 'mvn',
|
|
3288
3326
|
'pwsh', 'powershell', 'bash', 'sh',
|
|
3289
3327
|
'curl', 'wget',
|
|
@@ -3588,6 +3626,28 @@ function resolveAgentBareMode(agent, engine) {
|
|
|
3588
3626
|
return false;
|
|
3589
3627
|
}
|
|
3590
3628
|
|
|
3629
|
+
// W-mrdon0pe000l045a — resolve whether repo-authored CLAUDE.md instructions
|
|
3630
|
+
// should be propagated into the prompt for runtimes that don't auto-discover
|
|
3631
|
+
// CLAUDE.md natively (Copilot/Codex). Precedence mirrors
|
|
3632
|
+
// `resolveLiveCheckoutAutoReset`:
|
|
3633
|
+
// 1. per-project boolean `project.propagateClaudeMdForNonClaudeRuntimes`
|
|
3634
|
+
// 2. fleet-wide boolean `engine.propagateClaudeMdForNonClaudeRuntimes`
|
|
3635
|
+
// 3. hardcoded default TRUE (additive, on by default)
|
|
3636
|
+
// Only an explicit boolean counts as "set" at either level — undefined/null/
|
|
3637
|
+
// string are treated as unset so the next tier (or the TRUE default) decides.
|
|
3638
|
+
// This gate is orthogonal to each runtime's native AGENTS.md / repo-instruction
|
|
3639
|
+
// discovery by design (see ENGINE_DEFAULTS.propagateClaudeMdForNonClaudeRuntimes).
|
|
3640
|
+
// Pure, no I/O.
|
|
3641
|
+
function resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine) {
|
|
3642
|
+
if (project && typeof project === 'object' && typeof project.propagateClaudeMdForNonClaudeRuntimes === 'boolean') {
|
|
3643
|
+
return project.propagateClaudeMdForNonClaudeRuntimes;
|
|
3644
|
+
}
|
|
3645
|
+
if (engine && typeof engine === 'object' && typeof engine.propagateClaudeMdForNonClaudeRuntimes === 'boolean') {
|
|
3646
|
+
return engine.propagateClaudeMdForNonClaudeRuntimes;
|
|
3647
|
+
}
|
|
3648
|
+
return true;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3591
3651
|
// ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
|
|
3592
3652
|
//
|
|
3593
3653
|
// Pre-P-3b8e5f1d, `engine.ccModel` was the single fleet-wide model knob (it
|
|
@@ -3902,6 +3962,15 @@ const WORK_TYPE = {
|
|
|
3902
3962
|
BUILD_FIX_COMPLEX: 'build-fix-complex',
|
|
3903
3963
|
};
|
|
3904
3964
|
|
|
3965
|
+
const FIX_LIKE_WORK_TYPES = new Set([
|
|
3966
|
+
WORK_TYPE.FIX,
|
|
3967
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
3968
|
+
]);
|
|
3969
|
+
|
|
3970
|
+
function isFixLikeWorkType(type) {
|
|
3971
|
+
return FIX_LIKE_WORK_TYPES.has(type);
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3905
3974
|
// Work types whose dispatch path requires a per-project git worktree. The
|
|
3906
3975
|
// engine's spawnAgent uses the project's `localPath` as the worktree root —
|
|
3907
3976
|
// without an owning project the rootDir falls back to MINIONS_DIR's parent,
|
|
@@ -3940,6 +4009,21 @@ const WORKTREE_REQUIRING_TYPES = new Set([
|
|
|
3940
4009
|
WORK_TYPE.DOCS,
|
|
3941
4010
|
]);
|
|
3942
4011
|
|
|
4012
|
+
// Hybrid routing is meaningful only for work types that would otherwise need
|
|
4013
|
+
// an isolated project worktree. Read-only root tasks already run from the
|
|
4014
|
+
// project/root path and therefore gain nothing from a live-validation entry.
|
|
4015
|
+
const LIVE_VALIDATION_WORK_TYPES = new Set(WORKTREE_REQUIRING_TYPES);
|
|
4016
|
+
|
|
4017
|
+
function resolveLiveValidationAutoDispatchType(project) {
|
|
4018
|
+
if (!project?.liveValidation || project.liveValidation.autoDispatch !== true) return null;
|
|
4019
|
+
if (resolveCheckoutMode(project) !== CHECKOUT_MODES.LIVE) return null;
|
|
4020
|
+
const inspection = inspectLiveValidation(project.liveValidation);
|
|
4021
|
+
if (inspection.error) return null;
|
|
4022
|
+
return inspection.types.includes(WORK_TYPE.TEST)
|
|
4023
|
+
? WORK_TYPE.TEST
|
|
4024
|
+
: null;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
3943
4027
|
// W-mpxqkkn300121d21 — Valid `type` strings the materializer will honor when a
|
|
3944
4028
|
// PRD missing_feature carries one. Excludes orchestration-only types that the
|
|
3945
4029
|
// plan→PRD→materializer pipeline never produces (PLAN, PLAN_TO_PRD, MEETING)
|
|
@@ -4435,7 +4519,7 @@ const FAILURE_CLASS = {
|
|
|
4435
4519
|
OUTPUT_TRUNCATED: 'output-truncated', // P-8e4c2a17: the agent streamed more stdout than the engine's hard capture cap (engine.js AGENT_OUTPUT_CAP_BYTES, 1MB) BEFORE the terminal `result` event arrived. The result (session id, completion block, final text) lives at the END of the stream, so it fell outside the captured window and parseOutput found nothing — the dispatch would otherwise fail as an opaque UNKNOWN and retry to death. Surfaced loudly with an actionable message; non-retryable (mechanical retry just reproduces the overflow — the agent must reduce output volume or the task must be split).
|
|
4436
4520
|
VERIFY_MISSING_PR: 'verify-missing-pr', // Verify completed without an attached or tracked PR.
|
|
4437
4521
|
PROJECT_NOT_FOUND: 'project_not_found', // W-mqv2wsy30002e090: a work item's `project` field names a project that is not configured (resolveConfiguredProject / formatUnknownProjectError → `Project "<name>" not found. Known projects: …`). Stamped at discovery time so the dashboard PR-column renderer surfaces the red failReason snippet like other non-retryable failures instead of burying it in the Agent column. Non-retryable — operator must fix the WI's project field or configure the project.
|
|
4438
|
-
PRE_DISPATCH_EVAL_STUCK: 'pre-dispatch-eval-stuck', //
|
|
4522
|
+
PRE_DISPATCH_EVAL_STUCK: 'pre-dispatch-eval-stuck', // Legacy persisted classification normalized by migration 025. New exhausted evaluations remain pending with _preDispatchEval.exhausted so they are not counted as agent execution failures.
|
|
4439
4523
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
4440
4524
|
};
|
|
4441
4525
|
|
|
@@ -7295,7 +7379,12 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
7295
7379
|
// { skipped: true, reason } — not a pr-requiring type / no ref / no URL / upsert error
|
|
7296
7380
|
// { alreadyEnrolled: true, id } — PR already tracked
|
|
7297
7381
|
// { enrolled: true, id, scope } — newly enrolled
|
|
7298
|
-
const _PR_REQUIRING_TYPES = new Set([
|
|
7382
|
+
const _PR_REQUIRING_TYPES = new Set([
|
|
7383
|
+
WORK_TYPE.FIX,
|
|
7384
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
7385
|
+
WORK_TYPE.REVIEW,
|
|
7386
|
+
WORK_TYPE.TEST,
|
|
7387
|
+
]);
|
|
7299
7388
|
function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
7300
7389
|
if (!item || !_PR_REQUIRING_TYPES.has(item.type)) return { skipped: true, reason: 'not-pr-requiring-type' };
|
|
7301
7390
|
const prRef = extractStructuredWorkItemPrRef(item);
|
|
@@ -8421,12 +8510,12 @@ module.exports = {
|
|
|
8421
8510
|
ENGINE_DEFAULTS,
|
|
8422
8511
|
resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
|
|
8423
8512
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentUseWorkerPool, resolveAgentAcpPoolSize, resolveAgentModel, resolveCcModel,
|
|
8424
|
-
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
8513
|
+
resolveAgentMaxBudget, resolveAgentBareMode, resolvePropagateClaudeMdForNonClaudeRuntimes,
|
|
8425
8514
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
8426
8515
|
runtimeConfigWarnings,
|
|
8427
8516
|
projectWorkSourceWarnings,
|
|
8428
8517
|
backfillProjectWorkSourceDefaults,
|
|
8429
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, WORKTREE_REQUIRING_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8518
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8430
8519
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
8431
8520
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
8432
8521
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
@@ -8546,6 +8635,10 @@ module.exports = {
|
|
|
8546
8635
|
CHECKOUT_MODES,
|
|
8547
8636
|
validateCheckoutMode,
|
|
8548
8637
|
validateLiveValidation,
|
|
8638
|
+
normalizeLiveValidationWorkType,
|
|
8639
|
+
resolveLiveValidationTypes,
|
|
8640
|
+
isLiveValidationConfigInvalid,
|
|
8641
|
+
resolveLiveValidationAutoDispatchType,
|
|
8549
8642
|
resolveCheckoutMode,
|
|
8550
8643
|
isLiveCheckoutProject,
|
|
8551
8644
|
resolveLiveCheckoutAutoReset,
|