@yemi33/minions 0.1.2380 → 0.1.2381
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 +1 -1
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +42 -47
- package/docs/live-checkout-mode.md +45 -26
- package/engine/dispatch.js +5 -3
- package/engine/lifecycle.js +59 -72
- package/engine/live-checkout.js +193 -149
- package/engine/playbook.js +12 -15
- package/engine/routing.js +1 -1
- package/engine/shared.js +136 -62
- package/engine.js +50 -15
- package/package.json +1 -1
- package/prompts/cc-system.md +7 -7
package/engine/shared.js
CHANGED
|
@@ -2481,6 +2481,80 @@ function classifyInboxItem(name, content) {
|
|
|
2481
2481
|
// config.json keep working without an on-disk rewrite. Tracked in
|
|
2482
2482
|
// docs/deprecated.json (id: worktreemode-field-rename).
|
|
2483
2483
|
const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
2484
|
+
const LEGACY_LIVE_VALIDATION_TYPE_ALIASES = Object.freeze({
|
|
2485
|
+
'build-and-test': 'test',
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2488
|
+
function normalizeLiveValidationWorkType(value) {
|
|
2489
|
+
if (typeof value !== 'string') return '';
|
|
2490
|
+
const type = value.trim();
|
|
2491
|
+
return LEGACY_LIVE_VALIDATION_TYPE_ALIASES[type] || type;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function inspectLiveValidation(value) {
|
|
2495
|
+
if (value === undefined || value === null || value === '') {
|
|
2496
|
+
return { configured: false, types: [], normalizedType: undefined, error: null };
|
|
2497
|
+
}
|
|
2498
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
2499
|
+
return {
|
|
2500
|
+
configured: true,
|
|
2501
|
+
types: [],
|
|
2502
|
+
normalizedType: undefined,
|
|
2503
|
+
error: 'Invalid liveValidation: must be an object { type, autoDispatch }.',
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const rawType = value.type;
|
|
2508
|
+
const rawTypes = Array.isArray(rawType) ? rawType : [rawType];
|
|
2509
|
+
if (
|
|
2510
|
+
(typeof rawType !== 'string' && !Array.isArray(rawType))
|
|
2511
|
+
|| rawTypes.length === 0
|
|
2512
|
+
|| !rawTypes.every(type => typeof type === 'string' && type.trim())
|
|
2513
|
+
) {
|
|
2514
|
+
const accepted = [...LIVE_VALIDATION_WORK_TYPES].sort().join(', ');
|
|
2515
|
+
return {
|
|
2516
|
+
configured: true,
|
|
2517
|
+
types: [],
|
|
2518
|
+
normalizedType: undefined,
|
|
2519
|
+
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}.`,
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
const normalizedTypes = [...new Set(rawTypes.map(normalizeLiveValidationWorkType))];
|
|
2524
|
+
const types = normalizedTypes.filter(type => LIVE_VALIDATION_WORK_TYPES.has(type));
|
|
2525
|
+
const invalidTypes = normalizedTypes.filter(type => !LIVE_VALIDATION_WORK_TYPES.has(type));
|
|
2526
|
+
let error = null;
|
|
2527
|
+
if (invalidTypes.length > 0) {
|
|
2528
|
+
const accepted = [...LIVE_VALIDATION_WORK_TYPES].sort().join(', ');
|
|
2529
|
+
error = `Invalid liveValidation type(s): ${invalidTypes.join(', ')}. Accepted values: ${accepted}.`;
|
|
2530
|
+
}
|
|
2531
|
+
if (!error && Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
2532
|
+
if (typeof value.autoDispatch !== 'boolean') {
|
|
2533
|
+
error = 'Invalid liveValidation: "autoDispatch" must be a boolean.';
|
|
2534
|
+
} else if (value.autoDispatch && !normalizedTypes.includes(WORK_TYPE.TEST)) {
|
|
2535
|
+
error = 'Invalid liveValidation: autoDispatch requires "test" in liveValidation.type so the generated validation work item runs on the live checkout.';
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
return {
|
|
2539
|
+
configured: true,
|
|
2540
|
+
types,
|
|
2541
|
+
normalizedType: Array.isArray(rawType) ? normalizedTypes : normalizedTypes[0],
|
|
2542
|
+
error,
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// Return the canonical, live-capable work-item types configured for a hybrid
|
|
2547
|
+
// project. Raw config is read defensively because config.json can predate the
|
|
2548
|
+
// validator; invalid entries are ignored rather than turning hybrid into full
|
|
2549
|
+
// live mode. `build-and-test` is retained as a read-compat alias for `test`.
|
|
2550
|
+
function resolveLiveValidationTypes(project) {
|
|
2551
|
+
return inspectLiveValidation(project && project.liveValidation).types;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function isLiveValidationConfigInvalid(project) {
|
|
2555
|
+
const inspection = inspectLiveValidation(project && project.liveValidation);
|
|
2556
|
+
return inspection.configured && !!inspection.error;
|
|
2557
|
+
}
|
|
2484
2558
|
|
|
2485
2559
|
// Resolve a project's effective checkout mode, honoring the legacy
|
|
2486
2560
|
// `worktreeMode` field for back-compat. Reading order:
|
|
@@ -2492,37 +2566,37 @@ const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
|
2492
2566
|
//
|
|
2493
2567
|
// Optional second argument `workItemType` enables liveValidation routing:
|
|
2494
2568
|
// If checkoutMode === 'live' AND project.liveValidation is set:
|
|
2495
|
-
// - workItemType matches liveValidation.type → 'live'
|
|
2496
|
-
// - otherwise → 'worktree'
|
|
2569
|
+
// - workItemType matches liveValidation.type → 'live'
|
|
2570
|
+
// - otherwise → 'worktree'
|
|
2497
2571
|
// liveValidation without checkoutMode: 'live' is ignored with a warn log.
|
|
2498
2572
|
function resolveCheckoutMode(project, workItemType) {
|
|
2499
2573
|
if (!project || typeof project !== 'object') return CHECKOUT_MODES.WORKTREE;
|
|
2500
2574
|
const canonical = project.checkoutMode;
|
|
2575
|
+
const legacy = project.worktreeMode;
|
|
2576
|
+
let effectiveMode = CHECKOUT_MODES.WORKTREE;
|
|
2577
|
+
if (canonical === CHECKOUT_MODES.LIVE || canonical === CHECKOUT_MODES.WORKTREE) {
|
|
2578
|
+
effectiveMode = canonical;
|
|
2579
|
+
} else if (legacy === 'live') {
|
|
2580
|
+
effectiveMode = CHECKOUT_MODES.LIVE;
|
|
2581
|
+
}
|
|
2501
2582
|
// liveValidation without checkoutMode: 'live' is a misconfiguration — warn and ignore.
|
|
2502
|
-
if (project.liveValidation &&
|
|
2583
|
+
if (project.liveValidation && effectiveMode !== CHECKOUT_MODES.LIVE) {
|
|
2503
2584
|
log('warn', 'resolveCheckoutMode: liveValidation is set but checkoutMode is not "live" — liveValidation ignored',
|
|
2504
2585
|
{ projectName: project.name });
|
|
2505
2586
|
}
|
|
2506
|
-
if (
|
|
2587
|
+
if (effectiveMode === CHECKOUT_MODES.LIVE) {
|
|
2507
2588
|
// Apply liveValidation routing when the block is present and workItemType is provided.
|
|
2508
2589
|
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
2509
2590
|
// array of strings (W-mr2m1ute000a9c01, multi-select hybrid types) —
|
|
2510
2591
|
// normalize to an array and do a membership check so either shape works.
|
|
2511
2592
|
if (project.liveValidation && workItemType !== undefined) {
|
|
2512
|
-
const lvTypes =
|
|
2513
|
-
|
|
2514
|
-
: [project.liveValidation.type];
|
|
2515
|
-
return lvTypes.includes(workItemType)
|
|
2593
|
+
const lvTypes = resolveLiveValidationTypes(project);
|
|
2594
|
+
return lvTypes.includes(normalizeLiveValidationWorkType(workItemType))
|
|
2516
2595
|
? CHECKOUT_MODES.LIVE
|
|
2517
2596
|
: CHECKOUT_MODES.WORKTREE;
|
|
2518
2597
|
}
|
|
2519
2598
|
return CHECKOUT_MODES.LIVE;
|
|
2520
2599
|
}
|
|
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
2600
|
return CHECKOUT_MODES.WORKTREE;
|
|
2527
2601
|
}
|
|
2528
2602
|
|
|
@@ -2538,9 +2612,10 @@ function isLiveCheckoutProject(project) {
|
|
|
2538
2612
|
// anything else (undefined / null / string) is treated as unset so the next
|
|
2539
2613
|
// tier (or the `false` default) decides. When ON and a live-checkout tree is
|
|
2540
2614
|
// dirty, `engine/live-checkout.js#prepareLiveCheckout` runs
|
|
2541
|
-
// `git
|
|
2542
|
-
//
|
|
2543
|
-
// so it is opt-in
|
|
2615
|
+
// `git reset --hard HEAD` + `git clean -fd` instead of refusing with
|
|
2616
|
+
// LIVE_CHECKOUT_DIRTY. This discards uncommitted tracked and untracked work but
|
|
2617
|
+
// preserves the current branch and committed history, so it is opt-in. Sibling
|
|
2618
|
+
// of `resolveCheckoutMode` — pure, no I/O.
|
|
2544
2619
|
function resolveLiveCheckoutAutoReset(project, engine) {
|
|
2545
2620
|
if (project && typeof project === 'object' && typeof project.liveCheckoutAutoReset === 'boolean') {
|
|
2546
2621
|
return project.liveCheckoutAutoReset;
|
|
@@ -2605,16 +2680,14 @@ function validateCheckoutMode(value) {
|
|
|
2605
2680
|
}
|
|
2606
2681
|
|
|
2607
2682
|
// 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.
|
|
2683
|
+
// Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }: only
|
|
2684
|
+
// work items whose type is IN liveValidation.type run in-place; all other types
|
|
2685
|
+
// use isolated worktrees. See resolveCheckoutMode and docs/live-checkout-mode.md.
|
|
2613
2686
|
//
|
|
2614
2687
|
// `type` may be a single work-item-type string (legacy / back-compat) OR a
|
|
2615
2688
|
// non-empty array of non-empty-string work-item types
|
|
2616
2689
|
// (W-mr2m1ute000a9c01 — multi-select hybrid types, e.g. both "implement" and
|
|
2617
|
-
// "fix"
|
|
2690
|
+
// "fix" run live while other types stay in worktrees). Every reader of
|
|
2618
2691
|
// liveValidation.type (resolveCheckoutMode, lifecycle.autoDispatchLiveValidationWi,
|
|
2619
2692
|
// dashboard project mapper / pill / CC label) must handle BOTH shapes —
|
|
2620
2693
|
// existing config.json entries with a plain string `type` continue to work
|
|
@@ -2623,10 +2696,8 @@ function validateCheckoutMode(value) {
|
|
|
2623
2696
|
// Returns:
|
|
2624
2697
|
// - undefined → caller should clear the field (empty / null / '' input)
|
|
2625
2698
|
// - { 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.
|
|
2699
|
+
// the caller's input shape (string stays a string; array stays an array),
|
|
2700
|
+
// with values trimmed, normalized, and deduplicated.
|
|
2630
2701
|
// Throws HTTP 400 (via _httpError) on malformed input, or when the effective
|
|
2631
2702
|
// checkout mode is not 'live' (liveValidation is meaningless without it — it is
|
|
2632
2703
|
// silently ignored at resolve time, so we refuse to persist a misconfiguration).
|
|
@@ -2636,45 +2707,15 @@ function validateCheckoutMode(value) {
|
|
|
2636
2707
|
// applied so the gate sees the intended final mode.
|
|
2637
2708
|
function validateLiveValidation(value, opts) {
|
|
2638
2709
|
if (value === undefined || value === null || value === '') return undefined;
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2710
|
+
const inspection = inspectLiveValidation(value);
|
|
2711
|
+
if (inspection.error) throw _httpError(400, inspection.error);
|
|
2642
2712
|
const checkoutMode = opts && opts.checkoutMode;
|
|
2643
2713
|
if (checkoutMode !== CHECKOUT_MODES.LIVE) {
|
|
2644
2714
|
throw _httpError(400, 'liveValidation requires checkoutMode "live" (hybrid mode). Set checkoutMode to "live" first, or clear liveValidation.');
|
|
2645
2715
|
}
|
|
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.
|
|
2716
|
+
const out = { type: inspection.normalizedType };
|
|
2676
2717
|
if (Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
2677
|
-
out.autoDispatch =
|
|
2718
|
+
out.autoDispatch = value.autoDispatch;
|
|
2678
2719
|
}
|
|
2679
2720
|
return out;
|
|
2680
2721
|
}
|
|
@@ -3902,6 +3943,15 @@ const WORK_TYPE = {
|
|
|
3902
3943
|
BUILD_FIX_COMPLEX: 'build-fix-complex',
|
|
3903
3944
|
};
|
|
3904
3945
|
|
|
3946
|
+
const FIX_LIKE_WORK_TYPES = new Set([
|
|
3947
|
+
WORK_TYPE.FIX,
|
|
3948
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
3949
|
+
]);
|
|
3950
|
+
|
|
3951
|
+
function isFixLikeWorkType(type) {
|
|
3952
|
+
return FIX_LIKE_WORK_TYPES.has(type);
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3905
3955
|
// Work types whose dispatch path requires a per-project git worktree. The
|
|
3906
3956
|
// engine's spawnAgent uses the project's `localPath` as the worktree root —
|
|
3907
3957
|
// without an owning project the rootDir falls back to MINIONS_DIR's parent,
|
|
@@ -3940,6 +3990,21 @@ const WORKTREE_REQUIRING_TYPES = new Set([
|
|
|
3940
3990
|
WORK_TYPE.DOCS,
|
|
3941
3991
|
]);
|
|
3942
3992
|
|
|
3993
|
+
// Hybrid routing is meaningful only for work types that would otherwise need
|
|
3994
|
+
// an isolated project worktree. Read-only root tasks already run from the
|
|
3995
|
+
// project/root path and therefore gain nothing from a live-validation entry.
|
|
3996
|
+
const LIVE_VALIDATION_WORK_TYPES = new Set(WORKTREE_REQUIRING_TYPES);
|
|
3997
|
+
|
|
3998
|
+
function resolveLiveValidationAutoDispatchType(project) {
|
|
3999
|
+
if (!project?.liveValidation || project.liveValidation.autoDispatch !== true) return null;
|
|
4000
|
+
if (resolveCheckoutMode(project) !== CHECKOUT_MODES.LIVE) return null;
|
|
4001
|
+
const inspection = inspectLiveValidation(project.liveValidation);
|
|
4002
|
+
if (inspection.error) return null;
|
|
4003
|
+
return inspection.types.includes(WORK_TYPE.TEST)
|
|
4004
|
+
? WORK_TYPE.TEST
|
|
4005
|
+
: null;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
3943
4008
|
// W-mpxqkkn300121d21 — Valid `type` strings the materializer will honor when a
|
|
3944
4009
|
// PRD missing_feature carries one. Excludes orchestration-only types that the
|
|
3945
4010
|
// plan→PRD→materializer pipeline never produces (PLAN, PLAN_TO_PRD, MEETING)
|
|
@@ -7295,7 +7360,12 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
7295
7360
|
// { skipped: true, reason } — not a pr-requiring type / no ref / no URL / upsert error
|
|
7296
7361
|
// { alreadyEnrolled: true, id } — PR already tracked
|
|
7297
7362
|
// { enrolled: true, id, scope } — newly enrolled
|
|
7298
|
-
const _PR_REQUIRING_TYPES = new Set([
|
|
7363
|
+
const _PR_REQUIRING_TYPES = new Set([
|
|
7364
|
+
WORK_TYPE.FIX,
|
|
7365
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
7366
|
+
WORK_TYPE.REVIEW,
|
|
7367
|
+
WORK_TYPE.TEST,
|
|
7368
|
+
]);
|
|
7299
7369
|
function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
7300
7370
|
if (!item || !_PR_REQUIRING_TYPES.has(item.type)) return { skipped: true, reason: 'not-pr-requiring-type' };
|
|
7301
7371
|
const prRef = extractStructuredWorkItemPrRef(item);
|
|
@@ -8426,7 +8496,7 @@ module.exports = {
|
|
|
8426
8496
|
runtimeConfigWarnings,
|
|
8427
8497
|
projectWorkSourceWarnings,
|
|
8428
8498
|
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,
|
|
8499
|
+
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
8500
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
8431
8501
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
8432
8502
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
@@ -8546,6 +8616,10 @@ module.exports = {
|
|
|
8546
8616
|
CHECKOUT_MODES,
|
|
8547
8617
|
validateCheckoutMode,
|
|
8548
8618
|
validateLiveValidation,
|
|
8619
|
+
normalizeLiveValidationWorkType,
|
|
8620
|
+
resolveLiveValidationTypes,
|
|
8621
|
+
isLiveValidationConfigInvalid,
|
|
8622
|
+
resolveLiveValidationAutoDispatchType,
|
|
8549
8623
|
resolveCheckoutMode,
|
|
8550
8624
|
isLiveCheckoutProject,
|
|
8551
8625
|
resolveLiveCheckoutAutoReset,
|
package/engine.js
CHANGED
|
@@ -2711,7 +2711,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2711
2711
|
// auto-stash is disabled, or because `applyLiveCheckoutAutoStash` itself
|
|
2712
2712
|
// reported `outcome:'unchanged'` (the `git stash push` call failed) — and
|
|
2713
2713
|
// the fleet/project auto-reset policy resolves true, NOW perform the
|
|
2714
|
-
// destructive `
|
|
2714
|
+
// destructive `reset --hard HEAD` + `clean -fd` WIP cleanup as a fallback, reusing
|
|
2715
2715
|
// prepareLiveCheckout's existing reset logic by re-invoking it with an
|
|
2716
2716
|
// explicit `autoReset: true` (bypassing config resolution, which we've
|
|
2717
2717
|
// already evaluated). This only fires when the first prepareLiveCheckout
|
|
@@ -2761,6 +2761,27 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2761
2761
|
if (_liveResult && _liveResult.ok === false && _liveResult.reason === 'dirty') {
|
|
2762
2762
|
const _dirtyFiles = Array.isArray(_liveResult.dirtyFiles) ? _liveResult.dirtyFiles : [];
|
|
2763
2763
|
const _branchInfo = typeof _liveResult.branchInfo === 'string' ? _liveResult.branchInfo : '';
|
|
2764
|
+
const _autoResetAttempt = _liveResult.autoResetAttempt;
|
|
2765
|
+
const _resetAuditSlug = typeof _autoResetAttempt?.auditSlug === 'string'
|
|
2766
|
+
? _autoResetAttempt.auditSlug
|
|
2767
|
+
: `live-checkout-autoreset-${id}`;
|
|
2768
|
+
let _cleanAttemptStatus = 'not run';
|
|
2769
|
+
let _statusCheckStatus = 'failed';
|
|
2770
|
+
if (_autoResetAttempt?.cleanAttempted) {
|
|
2771
|
+
_cleanAttemptStatus = _autoResetAttempt.cleanSucceeded ? 'succeeded' : 'failed or partially applied';
|
|
2772
|
+
}
|
|
2773
|
+
if (_autoResetAttempt?.recheckSucceeded) {
|
|
2774
|
+
_statusCheckStatus = `${_dirtyFiles.length} dirty path(s) remain`;
|
|
2775
|
+
}
|
|
2776
|
+
const _resetAttemptSummary = _autoResetAttempt
|
|
2777
|
+
? [
|
|
2778
|
+
'Automatic reset cleanup was attempted before this refusal:',
|
|
2779
|
+
`- \`git reset --hard HEAD\`: ${_autoResetAttempt.resetSucceeded ? 'succeeded' : 'failed'}`,
|
|
2780
|
+
`- \`git clean -fd\`: ${_cleanAttemptStatus}`,
|
|
2781
|
+
`- post-cleanup status check: ${_statusCheckStatus}`,
|
|
2782
|
+
`See the \`${_resetAuditSlug}\` inbox note for the full before/after audit.`,
|
|
2783
|
+
].join('\n')
|
|
2784
|
+
: 'No automatic reset/clean recovery was attempted before this refusal.';
|
|
2764
2785
|
// #329 + PL-live-checkout-reliability-hardening: two-strike dirty guard.
|
|
2765
2786
|
// First dirty failure is retried once (a transient or engine-owned dirty
|
|
2766
2787
|
// state — e.g. an agent's own leftover WIP, which dispatch-end restore now
|
|
@@ -2800,7 +2821,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2800
2821
|
`**Dispatch:** ${id}`,
|
|
2801
2822
|
...(_branchInfo ? ['', `**Current checkout:** \`${_branchInfo}\``] : []),
|
|
2802
2823
|
'',
|
|
2803
|
-
`The engine refused to spawn the agent because \`${cwd}\` has uncommitted changes. Live-checkout mode runs in your project directly
|
|
2824
|
+
`The engine refused to spawn the agent because \`${cwd}\` has uncommitted changes. Live-checkout mode runs in your project directly.`,
|
|
2804
2825
|
'',
|
|
2805
2826
|
'## Dirty files (`git status --porcelain=v1 -b`)',
|
|
2806
2827
|
'',
|
|
@@ -2808,6 +2829,8 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2808
2829
|
...(_dirtyFiles.length > 0 ? _liveCheckout.capDirtyFileLines(_dirtyFiles) : ['(none reported)']),
|
|
2809
2830
|
'```',
|
|
2810
2831
|
'',
|
|
2832
|
+
_resetAttemptSummary,
|
|
2833
|
+
'',
|
|
2811
2834
|
_autoCleanupEnabled
|
|
2812
2835
|
? 'Auto-cleanup (`liveCheckoutAutoReset`/`liveCheckoutAutoStash`) is active — the engine re-cleans the tree on each attempt, so this dispatch will be retried automatically (subject to the normal retry cap).'
|
|
2813
2836
|
: (_alreadyDirtyFailed
|
|
@@ -4394,7 +4417,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4394
4417
|
// agent never gets a chance to push over the rebased tip.
|
|
4395
4418
|
// Read-only and non-fix dispatches are out of scope — implement tasks cut
|
|
4396
4419
|
// their own branch from main, and review/verify don't push.
|
|
4397
|
-
if (type
|
|
4420
|
+
if (shared.isFixLikeWorkType(type) && branchName && worktreePath && cwd === worktreePath) {
|
|
4398
4421
|
_phaseT.staleHeadStart = Date.now();
|
|
4399
4422
|
try {
|
|
4400
4423
|
const guard = await assertStaleHeadOk({
|
|
@@ -8681,7 +8704,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8681
8704
|
return b && b > 0 && getMonthlySpend(id) >= b && isAgentIdle(id);
|
|
8682
8705
|
});
|
|
8683
8706
|
if (!agentId) {
|
|
8684
|
-
if (!budgetBlocked && !hardPinRequested && workType
|
|
8707
|
+
if (!budgetBlocked && !hardPinRequested && !shared.isFixLikeWorkType(workType)) {
|
|
8685
8708
|
reservedAgentId = resolveAgentReservation(workType, config, { agentHints });
|
|
8686
8709
|
agentId = reservedAgentId && !hasAgentHints ? routing.ANY_AGENT : reservedAgentId;
|
|
8687
8710
|
}
|
|
@@ -8723,7 +8746,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8723
8746
|
const linkedPr = item.skipPr ? null : resolveWorkItemPrRecord(item, project);
|
|
8724
8747
|
const promptItem = linkedPr ? withWorkItemPrContext(item, linkedPr) : item;
|
|
8725
8748
|
const prBranch = linkedPr?.branch || '';
|
|
8726
|
-
const isPrTargeted = !!(linkedPr && (workType
|
|
8749
|
+
const isPrTargeted = !!(linkedPr && (shared.isFixLikeWorkType(workType) || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST));
|
|
8727
8750
|
// Issue #607 — `linkedPr` above resolves through the LOOSE
|
|
8728
8751
|
// extractWorkItemPrRef (title/first-paragraph-of-description text scan),
|
|
8729
8752
|
// which is fine for prompt-context enrichment (promptItem, above) that
|
|
@@ -8745,7 +8768,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8745
8768
|
// create-time stamp path keeps using the loose form — gate uses
|
|
8746
8769
|
// structured-only; stamp uses loose. See engine/shared.js
|
|
8747
8770
|
// extractStructuredWorkItemPrRef for the structured-source list.
|
|
8748
|
-
if (!linkedPr && getStructuredWorkItemPrRef(item) && (workType
|
|
8771
|
+
if (!linkedPr && getStructuredWorkItemPrRef(item) && (shared.isFixLikeWorkType(workType) || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST)) {
|
|
8749
8772
|
if (item._pendingReason !== 'pr_not_found') { item._pendingReason = 'pr_not_found'; needsWrite = true; }
|
|
8750
8773
|
log('warn', `Work item ${item.id} references PR ${getStructuredWorkItemPrRef(item)} but no tracked PR record was found`);
|
|
8751
8774
|
continue;
|
|
@@ -9048,7 +9071,7 @@ function buildWorkItemDispatchVars(item, vars, config, options = {}) {
|
|
|
9048
9071
|
// SHERLOC (P-mqyp0008v022w3x4): inject prior explore context for fix dispatches.
|
|
9049
9072
|
// When a completed explore WI has a reference to this fix WI, surface its
|
|
9050
9073
|
// resultSummary so the fix agent starts with pre-localized fault context.
|
|
9051
|
-
if (workType
|
|
9074
|
+
if (shared.isFixLikeWorkType(workType)) {
|
|
9052
9075
|
vars.prior_explore_context = resolvePriorExploreContext(item, config);
|
|
9053
9076
|
}
|
|
9054
9077
|
|
|
@@ -9375,7 +9398,7 @@ function discoverCentralWorkItems(config) {
|
|
|
9375
9398
|
const hardPinRequested = !!hardPinnedAgent;
|
|
9376
9399
|
const agentId = hardPinnedAgent
|
|
9377
9400
|
|| resolveAgent(workType, config, { agentHints })
|
|
9378
|
-
|| (!hardPinRequested && workType
|
|
9401
|
+
|| (!hardPinRequested && !shared.isFixLikeWorkType(workType) ? resolveAgentReservation(workType, config, { agentHints }) : null);
|
|
9379
9402
|
if (!agentId) continue;
|
|
9380
9403
|
|
|
9381
9404
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
@@ -9757,7 +9780,7 @@ async function discoverWork(config) {
|
|
|
9757
9780
|
// type-only filter below would silently start gating those
|
|
9758
9781
|
// previously-ungated sources too. Scope the gate back down to
|
|
9759
9782
|
// `meta.source === 'pr'` to preserve the original behavior exactly.
|
|
9760
|
-
const isGateEligible = (w) => w.meta?.source === 'pr' && (w.type
|
|
9783
|
+
const isGateEligible = (w) => w.meta?.source === 'pr' && (shared.isFixLikeWorkType(w.type) || w.type === WORK_TYPE.REVIEW);
|
|
9761
9784
|
const gated = items.filter(isGateEligible);
|
|
9762
9785
|
if (gated.length > 0) {
|
|
9763
9786
|
log('info', `Gating ${gated.length} fixes/reviews — implement items in progress and no free slots`);
|
|
@@ -9799,7 +9822,7 @@ async function discoverWork(config) {
|
|
|
9799
9822
|
}));
|
|
9800
9823
|
}
|
|
9801
9824
|
for (const w of toDispatch) {
|
|
9802
|
-
if (w.type
|
|
9825
|
+
if (shared.isFixLikeWorkType(w.type)) persistedFixes++;
|
|
9803
9826
|
else if (w.type === WORK_TYPE.REVIEW) persistedReviews++;
|
|
9804
9827
|
else persistedOther++;
|
|
9805
9828
|
}
|
|
@@ -9818,7 +9841,7 @@ async function discoverWork(config) {
|
|
|
9818
9841
|
if (config.engine?.prDiscoveryEnabled !== false) {
|
|
9819
9842
|
const prWork = await discoverFromPrs(config, project);
|
|
9820
9843
|
projectWork.push(...prWork.filter(w =>
|
|
9821
|
-
w.type
|
|
9844
|
+
shared.isFixLikeWorkType(w.type) || w.type === WORK_TYPE.REVIEW || w.type === WORK_TYPE.TEST));
|
|
9822
9845
|
}
|
|
9823
9846
|
|
|
9824
9847
|
// Side-effect: specs → work items (picked up below)
|
|
@@ -10038,7 +10061,7 @@ function persistPendingDispatchAgent(item) {
|
|
|
10038
10061
|
}
|
|
10039
10062
|
|
|
10040
10063
|
function isSoftFixDispatch(item) {
|
|
10041
|
-
return item?.type
|
|
10064
|
+
return shared.isFixLikeWorkType(item?.type) && !routing.isAgentHardPinned(item.meta?.item);
|
|
10042
10065
|
}
|
|
10043
10066
|
|
|
10044
10067
|
function resolveMaxConcurrent(config) {
|
|
@@ -10945,7 +10968,15 @@ async function tickInner() {
|
|
|
10945
10968
|
skipDirtyCheck: !!_precheckProjCfg.skipLiveCheckoutDirtyCheck,
|
|
10946
10969
|
});
|
|
10947
10970
|
} catch (e) { log('warn', `live-checkout precheck (dirty) failed for ${item.id}: ${e.message}`); }
|
|
10948
|
-
|
|
10971
|
+
const _autoStashEnabled = _liveCheckoutPrecheck.resolveLiveCheckoutAutoStash({
|
|
10972
|
+
project: _precheckProjCfg,
|
|
10973
|
+
engine: config.engine || {},
|
|
10974
|
+
});
|
|
10975
|
+
const _autoResetEnabled = shared.resolveLiveCheckoutAutoReset(
|
|
10976
|
+
_precheckProjCfg,
|
|
10977
|
+
config.engine || {},
|
|
10978
|
+
);
|
|
10979
|
+
if (_dirtyResult && _dirtyResult.dirty && !_autoStashEnabled && !_autoResetEnabled) {
|
|
10949
10980
|
_persistPendingReason('live_checkout_dirty');
|
|
10950
10981
|
try {
|
|
10951
10982
|
const _alertBody = [
|
|
@@ -11131,11 +11162,12 @@ async function tickInner() {
|
|
|
11131
11162
|
// post-dispatch active list so the annotation loop's reason is consistent
|
|
11132
11163
|
// with whatever actually got spawned this tick.
|
|
11133
11164
|
const postLiveProjectsInUse = new Set();
|
|
11165
|
+
const postProjects = shared.getProjects(config);
|
|
11134
11166
|
for (const d of (postDispatch.active || [])) {
|
|
11135
11167
|
if (READ_ONLY_ROOT_TASK_TYPES.has(d.type)) continue;
|
|
11136
11168
|
const projName = d.project || d.meta?.project?.name || null;
|
|
11137
11169
|
if (!projName) continue;
|
|
11138
|
-
const projCfg = shared.findProjectByName(
|
|
11170
|
+
const projCfg = shared.findProjectByName(postProjects, projName);
|
|
11139
11171
|
if (shared.resolveCheckoutMode(projCfg, d.type) === 'live') {
|
|
11140
11172
|
postLiveProjectsInUse.add(projName);
|
|
11141
11173
|
}
|
|
@@ -11162,7 +11194,10 @@ async function tickInner() {
|
|
|
11162
11194
|
&& !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
|
|
11163
11195
|
&& postLiveProjectsInUse.has(itemProjName)
|
|
11164
11196
|
) {
|
|
11165
|
-
|
|
11197
|
+
const projCfg = shared.findProjectByName(postProjects, itemProjName);
|
|
11198
|
+
if (shared.resolveCheckoutMode(projCfg, item.type) === 'live') {
|
|
11199
|
+
reason = 'live_checkout_busy';
|
|
11200
|
+
}
|
|
11166
11201
|
}
|
|
11167
11202
|
}
|
|
11168
11203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2381",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/prompts/cc-system.md
CHANGED
|
@@ -92,7 +92,7 @@ When genuinely in doubt about the size, delegate — agents have isolated worktr
|
|
|
92
92
|
### HARD STOP — never edit files yourself in a live/hybrid project
|
|
93
93
|
This overrides everything above, **including the direct-handling override**. For any project whose effective checkout mode is `live` (or `hybrid` for a code-authoring work-item type — `implement`/`fix`/`docs`/`decompose`), you MUST NOT use `Edit`/`Write`/`Bash` (or any tool) to mutate files inside that project's `localPath`, **regardless of task size** — not even a Small 1-line change, and not even when the human explicitly says to do it yourself. The Step-3 "do it yourself" allowance and the Step-1 direct-handling override do **not** apply to writes inside a live/hybrid checkout.
|
|
94
94
|
|
|
95
|
-
Why: live-mode projects have no worktree isolation — the engine runs agents in-place inside `localPath
|
|
95
|
+
Why: live-mode projects have no worktree isolation — the engine runs agents in-place inside `localPath` and caps dispatch to one mutating operation at a time. A stray CC edit is outside dispatch ownership: with dirty recovery disabled it causes a `live-checkout-dirty` refusal that blocks live work, while the default auto-stash policy can silently park that edit before the next dispatch, so the requested change is no longer present for the agent to validate or commit. Auto-stash/reset is a dispatch recovery mechanism, not ownership bookkeeping for CC edits.
|
|
96
96
|
|
|
97
97
|
Instead, when a change is needed in a live/hybrid project, **dispatch a normal work item** (`POST /api/work-items`, `type: "implement"` or `"fix"` as usual) and let the engine own the live-checkout dispatch path (dirty-tree refusal, single-mutating-dispatch cap, auto-stash/auto-reset). Do not try to shortcut it with your own edit.
|
|
98
98
|
|
|
@@ -333,23 +333,23 @@ The `X-CC-Turn-Id` header is the audit trail — the handler stamps it onto the
|
|
|
333
333
|
Every configured project has an effective **checkout mode** — surfaced in your state snapshot next to the project as `[worktree]`, `[live]`, or `[hybrid (live-validation: <type>)]`. It controls where dispatched agents run:
|
|
334
334
|
|
|
335
335
|
- **`worktree`** (default) — each dispatch gets its own isolated `git worktree`. Agents run fully in parallel; nothing touches the operator's working tree. Use for normal repos.
|
|
336
|
-
- **`live`** — agents run **in-place** inside the project's `localPath` (no worktree). The engine caps this to **one mutating dispatch at a time** per project
|
|
337
|
-
- **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block.
|
|
336
|
+
- **`live`** — agents run **in-place** inside the project's `localPath` (no worktree). The engine caps this to **one mutating dispatch at a time** per project. Dirty-tree behavior follows the configured auto-stash/reset policy (auto-stash defaults on); with recovery disabled, the item remains pending until the tree is clean. Use only when worktrees are unworkable.
|
|
337
|
+
- **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Work items not listed in `type` use isolated worktrees; listed live-capable canonical work-item types run in-place. The normal validation type is `test`. `build-and-test` is a playbook name, not a work-item type. `type` accepts one string or an array. When `autoDispatch:true`, `test` must be included; the engine creates one PR-targeted `test` WI with the `build-and-test` playbook after each eligible coding WI completes.
|
|
338
338
|
|
|
339
|
-
**CC never writes into a live/hybrid checkout itself.**
|
|
339
|
+
**CC never writes into a live/hybrid checkout itself.** A one-off CC edit leaves untracked operator dirt that the next dispatch may unexpectedly stash or may block on when recovery is disabled. For any needed change, **dispatch a work item** and let the engine own the live-checkout path; never `Edit`/`Write`/`Bash`-mutate files there yourself. Read-only inspection remains fine.
|
|
340
340
|
|
|
341
341
|
**When to recommend hybrid:** the project's build/test/validation genuinely cannot run in an isolated worktree (it needs the real checkout — submodules, a `repo`-managed tree, an emulator/dev-server bound to `localPath`, deep-path tooling), **but** you still want coding agents to work in parallel rather than serialize through the single live checkout. If the *whole* workflow must run on the real tree, use plain `live`. If nothing needs the real tree, stay on `worktree`.
|
|
342
342
|
|
|
343
343
|
**Configuring it** (via the projects array on `POST /api/settings` — never hand-edit `config.json`):
|
|
344
344
|
```bash
|
|
345
|
-
# Switch a project to hybrid mode (live checkout + deferred
|
|
345
|
+
# Switch a project to hybrid mode (live checkout + deferred test validation)
|
|
346
346
|
curl -s -X POST http://localhost:{{dashboard_port}}/api/settings \
|
|
347
347
|
-H 'Content-Type: application/json' -H 'X-CC-Turn-Id: {{cc_turn_id}}' \
|
|
348
|
-
-d '{"projects":[{"name":"<project>","checkoutMode":"live","liveValidation":{"type":"
|
|
348
|
+
-d '{"projects":[{"name":"<project>","checkoutMode":"live","liveValidation":{"type":"test","autoDispatch":true}}]}'
|
|
349
349
|
# Clear hybrid (back to plain live): pass "liveValidation": null
|
|
350
350
|
# Clear live entirely (back to worktree): pass "checkoutMode": "worktree" (also clear liveValidation)
|
|
351
351
|
```
|
|
352
|
-
The server validates: `liveValidation` requires `checkoutMode:"live"
|
|
352
|
+
The server validates: `liveValidation` requires `checkoutMode:"live"`, canonical live-capable work-item type(s), and a boolean `autoDispatch`; automatic validation additionally requires `test` in `type`. **Always confirm the mode switch with the user before applying it** — it changes how future dispatches run.
|
|
353
353
|
|
|
354
354
|
## GitHub auth
|
|
355
355
|
|