@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.
@@ -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)
@@ -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 { fs.rmSync(dirPath, { recursive: true, force: true }); return true; }
1041
- catch { return false; }
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
- // P-b7d4e8f2 Stale-lock check combines mtime age with PID liveness:
1443
- // 1. If mtime <= LOCK_STALE_MS → never reap (recently active).
1444
- // 2. If JSON-parsable {pid, ts}:
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 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
- if (mtimeAge > LOCK_STALE_MS) {
1454
- let holderPid = null;
1455
- try {
1456
- const raw = fs.readFileSync(lockPath, 'utf8');
1457
- const parsed = JSON.parse(raw);
1458
- if (parsed && Number.isFinite(parsed.pid) && parsed.pid > 0) {
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
- if (shouldReap) {
1472
- try {
1473
- fs.unlinkSync(lockPath);
1474
- } catch (unlinkErr) {
1475
- // ENOENT: another process deleted the lock between stat and unlink — safe to retry.
1476
- // EPERM on Windows: file is in 'pending delete' state from a concurrent
1477
- // reaper — the OS will finalize the delete shortly; retry will succeed.
1478
- const tolerable = unlinkErr.code === 'ENOENT' ||
1479
- (process.platform === 'win32' && unlinkErr.code === 'EPERM');
1480
- if (!tolerable) throw unlinkErr;
1481
- }
1482
- continue; // lock just removed retry immediately
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' (validation serialized)
2496
- // - otherwise → 'worktree' (coding escapes the live cap)
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 && canonical !== CHECKOUT_MODES.LIVE) {
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 (canonical === CHECKOUT_MODES.LIVE) {
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 = Array.isArray(project.liveValidation.type)
2513
- ? project.liveValidation.type
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 fetch origin` + `git reset --hard origin/<branch>` instead of refusing
2542
- // with LIVE_CHECKOUT_DIRTY (the reset DISCARDS the operator's uncommitted work,
2543
- // so it is opt-in). Sibling of `resolveCheckoutMode` — pure, no I/O.
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 }: coding
2609
- // work items author in isolated worktrees (escaping the live cap) while only
2610
- // work items whose type is IN liveValidation.type run in-place on the live
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" validate live while other types stay in worktrees). Every reader of
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
- // whatever shape the caller passed in (string stays a string; array stays
2627
- // a deduped array) we intentionally do NOT force single-string input
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
- if (typeof value !== 'object' || Array.isArray(value)) {
2640
- throw _httpError(400, 'Invalid liveValidation: must be an object { type, autoDispatch }.');
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 typeErrorMsg = 'Invalid liveValidation: "type" is required and must be a non-empty string, or a non-empty array of non-empty-string work-item types — the work-item type(s) that run on the live checkout (e.g. "build-and-test" or ["implement", "fix"]).';
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 = !!value.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 is flipped to `failed`
3083
- // with FAILURE_CLASS.PRE_DISPATCH_EVAL_STUCK instead of being silently
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', // W-mrcrh5hj0017d22f: pre-dispatch-eval rejected the SAME (unchanged) description ENGINE_DEFAULTS.preDispatchEvalMaxRejections times in a row without the WI status ever leaving pending — previously this burned an LLM validation call every discovery tick forever with nothing surfaced to the dashboard (constant-bug-bash's file-fixes stage wedged 16+ consecutive rejections, one/minute, until a human manually cancelled it). Engine now flips the WI to failed with this class so the stalled item is visible and stops silently re-evaluating; non-retryable until the description changes.
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([WORK_TYPE.FIX, WORK_TYPE.REVIEW, WORK_TYPE.TEST]);
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,