@yemi33/minions 0.1.1921 → 0.1.1922
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 +253 -11
- package/engine/dispatch.js +32 -6
- package/engine/pre-dispatch-eval.js +51 -11
- package/engine/shared.js +1 -1
- package/engine.js +50 -2
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -2443,6 +2443,144 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
|
|
|
2443
2443
|
};
|
|
2444
2444
|
}
|
|
2445
2445
|
|
|
2446
|
+
/**
|
|
2447
|
+
* Sub-task D of W-mp2w003600196c51 (CC perf): pool-routed implementation of
|
|
2448
|
+
* the doc-chat LLM call. Mirrors _invokeCcStreamViaPool (the CC streaming
|
|
2449
|
+
* pool path from sub-task C), shaped to plug into ccCall / ccCallStreaming
|
|
2450
|
+
* — the doc-chat call sites that live at module scope rather than inside
|
|
2451
|
+
* the SSE handler closure.
|
|
2452
|
+
*
|
|
2453
|
+
* Pool key: `doc-chat:` + sessionKey (filePath or title). Different file =
|
|
2454
|
+
* different key = natural eviction (the pool kills/respawns the worker on
|
|
2455
|
+
* tab change). For one-shot flows (`freshSession: true`, e.g. Create Plan
|
|
2456
|
+
* from meeting) we use a unique short-lived tab key so the call cannot
|
|
2457
|
+
* inherit (or pollute) the persistent doc-chat conversation for that file.
|
|
2458
|
+
*
|
|
2459
|
+
* Behavior contract (matches callLLMStreaming so callers stay unchanged):
|
|
2460
|
+
* - Returns a Promise resolving to
|
|
2461
|
+
* `{ text, sessionId, code, usage, raw, stderr }`.
|
|
2462
|
+
* - `.abort` attached to the returned Promise. Abort cancels the in-flight
|
|
2463
|
+
* ACP prompt AND closes the worker tab — same "user-disconnect = strong
|
|
2464
|
+
* signal, drop the warm process" semantics CC uses in
|
|
2465
|
+
* handleCommandCenterAbort. For one-shot tabs, the tab is closed in
|
|
2466
|
+
* `finalize()` regardless of abort so we don't leak workers.
|
|
2467
|
+
* - `onChunk` receives FULL ACCUMULATED text (the pool emits per-chunk
|
|
2468
|
+
* deltas; we accumulate before forwarding to match the existing
|
|
2469
|
+
* contract SSE consumers depend on).
|
|
2470
|
+
* - `usage` is `{}` because ACP `session/update` notifications don't
|
|
2471
|
+
* surface token counts; trackEngineUsage is a no-op on `{}`.
|
|
2472
|
+
* - Tool calls are not surfaced (sub-task B/C don't plumb `tool_call`
|
|
2473
|
+
* notifications into a callback). Matches CC's pool trade-off.
|
|
2474
|
+
* - Honors `timeoutMs`. On timeout: cancels the prompt, closes the tab
|
|
2475
|
+
* (so the next call rebuilds against a clean process), resolves with
|
|
2476
|
+
* `{ code: 1, stderr: 'doc-chat-pool: timeout after Xms' }`. The
|
|
2477
|
+
* legacy direct path's `timeout` kills the CLI process; closing the
|
|
2478
|
+
* tab is the pool equivalent.
|
|
2479
|
+
*
|
|
2480
|
+
* Why we deliberately skip `docSessions` updates on this path: pool ACP
|
|
2481
|
+
* session IDs are valid only inside the live worker process. Persisting
|
|
2482
|
+
* them risks the next call seeing a "session unchanged + _docHash
|
|
2483
|
+
* matches → skip document content" path after the idle reaper (10 min)
|
|
2484
|
+
* kills the worker, silently starving the new ACP session of the
|
|
2485
|
+
* document body. Always re-sending extraContext is correctness-safe; the
|
|
2486
|
+
* pool's warm-process saving is preserved regardless.
|
|
2487
|
+
*/
|
|
2488
|
+
function _invokeDocChatViaPool({ prompt, model, effort, engineConfig, systemPrompt, sessionKey, freshSession, timeoutMs, onChunk }) {
|
|
2489
|
+
const oneShot = !!freshSession;
|
|
2490
|
+
const tabKey = oneShot
|
|
2491
|
+
? 'doc-chat:fresh:' + shared.uid()
|
|
2492
|
+
: 'doc-chat:' + (sessionKey || 'default');
|
|
2493
|
+
let cancelled = false;
|
|
2494
|
+
let settled = false;
|
|
2495
|
+
let accumulated = '';
|
|
2496
|
+
let sessionHandle = null;
|
|
2497
|
+
let timeoutTimer = null;
|
|
2498
|
+
let resolveResult;
|
|
2499
|
+
const promise = new Promise((resolve) => { resolveResult = resolve; });
|
|
2500
|
+
const finalize = (envelope) => {
|
|
2501
|
+
if (settled) return;
|
|
2502
|
+
settled = true;
|
|
2503
|
+
if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null; }
|
|
2504
|
+
if (oneShot) {
|
|
2505
|
+
// One-shot worker: tear down so freshSession callers don't leak a
|
|
2506
|
+
// persistent ACP process per call.
|
|
2507
|
+
try { ccWorkerPool.closeTab(tabKey); } catch { /* swallow */ }
|
|
2508
|
+
}
|
|
2509
|
+
resolveResult(envelope);
|
|
2510
|
+
};
|
|
2511
|
+
promise.abort = () => {
|
|
2512
|
+
cancelled = true;
|
|
2513
|
+
try { sessionHandle && sessionHandle.cancel(); } catch { /* swallow */ }
|
|
2514
|
+
// User-disconnect = strong "give up this conversation" signal. Mirror
|
|
2515
|
+
// handleCommandCenterAbort's closeTab for the same reason: prevents
|
|
2516
|
+
// partial/cancelled assistant output from polluting the next turn's
|
|
2517
|
+
// ACP context. The cost (lose warmth on next visit) is accepted.
|
|
2518
|
+
try { ccWorkerPool.closeTab(tabKey); } catch { /* swallow */ }
|
|
2519
|
+
};
|
|
2520
|
+
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
2521
|
+
timeoutTimer = setTimeout(() => {
|
|
2522
|
+
try { sessionHandle && sessionHandle.cancel(); } catch { /* swallow */ }
|
|
2523
|
+
try { ccWorkerPool.closeTab(tabKey); } catch { /* swallow */ }
|
|
2524
|
+
finalize({
|
|
2525
|
+
text: accumulated,
|
|
2526
|
+
sessionId: sessionHandle ? sessionHandle.sessionId : null,
|
|
2527
|
+
code: 1,
|
|
2528
|
+
usage: {},
|
|
2529
|
+
raw: accumulated,
|
|
2530
|
+
stderr: `doc-chat-pool: timeout after ${timeoutMs}ms`,
|
|
2531
|
+
});
|
|
2532
|
+
}, timeoutMs);
|
|
2533
|
+
if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();
|
|
2534
|
+
}
|
|
2535
|
+
(async () => {
|
|
2536
|
+
try {
|
|
2537
|
+
sessionHandle = await ccWorkerPool.getSession({
|
|
2538
|
+
tabId: tabKey,
|
|
2539
|
+
model,
|
|
2540
|
+
effort,
|
|
2541
|
+
mcpServers: (engineConfig && engineConfig.mcpServers) || [],
|
|
2542
|
+
systemPromptHash: _docChatPromptHash,
|
|
2543
|
+
});
|
|
2544
|
+
} catch (err) {
|
|
2545
|
+
return finalize({
|
|
2546
|
+
text: '',
|
|
2547
|
+
sessionId: null,
|
|
2548
|
+
code: 1,
|
|
2549
|
+
usage: {},
|
|
2550
|
+
raw: '',
|
|
2551
|
+
stderr: String((err && err.message) || err || 'cc-worker-pool spawn failed'),
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
if (cancelled) {
|
|
2555
|
+
try { sessionHandle.cancel(); } catch { /* swallow */ }
|
|
2556
|
+
return finalize({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
|
|
2557
|
+
}
|
|
2558
|
+
await sessionHandle.stream(prompt, {
|
|
2559
|
+
systemPromptText: systemPrompt,
|
|
2560
|
+
onChunk: (delta) => {
|
|
2561
|
+
accumulated += delta;
|
|
2562
|
+
if (onChunk) {
|
|
2563
|
+
try { onChunk(accumulated); } catch { /* swallow */ }
|
|
2564
|
+
}
|
|
2565
|
+
},
|
|
2566
|
+
onDone: () => {
|
|
2567
|
+
finalize({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
|
|
2568
|
+
},
|
|
2569
|
+
onError: (err) => {
|
|
2570
|
+
finalize({
|
|
2571
|
+
text: accumulated,
|
|
2572
|
+
sessionId: sessionHandle.sessionId,
|
|
2573
|
+
code: cancelled ? 0 : 1,
|
|
2574
|
+
usage: {},
|
|
2575
|
+
raw: accumulated,
|
|
2576
|
+
stderr: String((err && err.message) || err || 'cc-worker-pool stream error'),
|
|
2577
|
+
});
|
|
2578
|
+
},
|
|
2579
|
+
});
|
|
2580
|
+
})();
|
|
2581
|
+
return promise;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2446
2584
|
/**
|
|
2447
2585
|
* Core LLM call — shared by CC panel and doc modals.
|
|
2448
2586
|
* @param {string} message - User message
|
|
@@ -2454,8 +2592,13 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
|
|
|
2454
2592
|
* @param {number} opts.timeout - Timeout in ms
|
|
2455
2593
|
* @param {number} opts.maxTurns - Max tool-use turns
|
|
2456
2594
|
* @param {string} opts.allowedTools - Comma-separated tool list
|
|
2595
|
+
* @param {boolean} [opts.freshSession] - When true (doc-chat one-shot flows like Create Plan
|
|
2596
|
+
* from meeting), pool-routed calls (sub-task D of W-mp2w003600196c51) use a unique
|
|
2597
|
+
* short-lived tabKey so the one-shot cannot inherit/pollute the persistent doc-chat
|
|
2598
|
+
* conversation for the same file. The legacy direct path is unaffected — freshSession
|
|
2599
|
+
* semantics there are owned by ccDocCall, which deletes docSessions before invoking.
|
|
2457
2600
|
*/
|
|
2458
|
-
async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, skipPreflight = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript, turnId } = {}) {
|
|
2601
|
+
async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, skipPreflight = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript, turnId, freshSession = false } = {}) {
|
|
2459
2602
|
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
2460
2603
|
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
2461
2604
|
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
@@ -2468,6 +2611,37 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2468
2611
|
}
|
|
2469
2612
|
}
|
|
2470
2613
|
|
|
2614
|
+
// Sub-task D of W-mp2w003600196c51: route doc-chat through the persistent
|
|
2615
|
+
// ACP worker pool when ccUseWorkerPool is on. The pool keeps one warm
|
|
2616
|
+
// process per `doc-chat:<sessionKey>` tabId, saving the session/new + MCP
|
|
2617
|
+
// boot cost on subsequent doc-chat turns. CC's path is wired separately
|
|
2618
|
+
// in _invokeCcStream (handleCommandCenterStream); CC's non-stream ccCall
|
|
2619
|
+
// path intentionally stays on the per-turn spawn (this branch is
|
|
2620
|
+
// store==='doc' only). We deliberately do NOT call updateSession() — pool
|
|
2621
|
+
// ACP session IDs aren't durable across worker eviction; persisting them
|
|
2622
|
+
// alongside docSessions._docHash would risk the next call hitting the
|
|
2623
|
+
// "doc unchanged → skip context" branch after the idle reaper killed the
|
|
2624
|
+
// worker, silently starving the new ACP session of the doc body.
|
|
2625
|
+
if (store === 'doc' && CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey) {
|
|
2626
|
+
const poolPrompt = (function buildPoolPrompt() {
|
|
2627
|
+
const parts = !skipStatePreamble ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
2628
|
+
if (extraContext) parts.push(extraContext);
|
|
2629
|
+
const turnHeader = _ccTurnHeaderPart(turnId);
|
|
2630
|
+
if (turnHeader) parts.push(turnHeader);
|
|
2631
|
+
parts.push(message);
|
|
2632
|
+
return parts.join('\n\n---\n\n');
|
|
2633
|
+
})();
|
|
2634
|
+
const p = _invokeDocChatViaPool({
|
|
2635
|
+
prompt: poolPrompt, sessionKey, model, effort: ccEffort,
|
|
2636
|
+
engineConfig: CONFIG.engine, systemPrompt,
|
|
2637
|
+
freshSession, timeoutMs: timeout,
|
|
2638
|
+
});
|
|
2639
|
+
if (onAbortReady) onAbortReady(p.abort);
|
|
2640
|
+
const result = await p;
|
|
2641
|
+
llm.trackEngineUsage(label, result.usage);
|
|
2642
|
+
return result;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2471
2645
|
const existing = resolveSession(store, sessionKey);
|
|
2472
2646
|
let sessionId = existing ? existing.sessionId : null;
|
|
2473
2647
|
const currentRuntime = shared.resolveCcCli(CONFIG.engine);
|
|
@@ -2570,7 +2744,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2570
2744
|
return result;
|
|
2571
2745
|
}
|
|
2572
2746
|
|
|
2573
|
-
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, skipPreflight = false, model, onAbortReady, onChunk, onToolUse, onRetry, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript, turnId } = {}) {
|
|
2747
|
+
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, skipPreflight = false, model, onAbortReady, onChunk, onToolUse, onRetry, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript, turnId, freshSession = false } = {}) {
|
|
2574
2748
|
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
2575
2749
|
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
2576
2750
|
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
@@ -2583,6 +2757,33 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2583
2757
|
}
|
|
2584
2758
|
}
|
|
2585
2759
|
|
|
2760
|
+
// Sub-task D of W-mp2w003600196c51: streaming counterpart of the ccCall
|
|
2761
|
+
// pool branch. See ccCall for the rationale; the streaming variant
|
|
2762
|
+
// additionally forwards onChunk so the SSE writer (handleDocChatStream)
|
|
2763
|
+
// keeps receiving accumulated text per the callLLMStreaming contract.
|
|
2764
|
+
// We also do NOT call updateSession() — see ccCall for the
|
|
2765
|
+
// docSessions-eviction-vs-_docHash staleness rationale.
|
|
2766
|
+
if (store === 'doc' && CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey) {
|
|
2767
|
+
const poolPrompt = (function buildPoolPrompt() {
|
|
2768
|
+
const parts = !skipStatePreamble ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
2769
|
+
if (extraContext) parts.push(extraContext);
|
|
2770
|
+
const turnHeader = _ccTurnHeaderPart(turnId);
|
|
2771
|
+
if (turnHeader) parts.push(turnHeader);
|
|
2772
|
+
parts.push(message);
|
|
2773
|
+
return parts.join('\n\n---\n\n');
|
|
2774
|
+
})();
|
|
2775
|
+
const p = _invokeDocChatViaPool({
|
|
2776
|
+
prompt: poolPrompt, sessionKey, model, effort: ccEffort,
|
|
2777
|
+
engineConfig: CONFIG.engine, systemPrompt,
|
|
2778
|
+
onChunk,
|
|
2779
|
+
freshSession, timeoutMs: timeout,
|
|
2780
|
+
});
|
|
2781
|
+
if (onAbortReady) onAbortReady(p.abort);
|
|
2782
|
+
const result = await p;
|
|
2783
|
+
llm.trackEngineUsage(label, result.usage);
|
|
2784
|
+
return result;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2586
2787
|
const existing = resolveSession(store, sessionKey);
|
|
2587
2788
|
let sessionId = existing ? existing.sessionId : null;
|
|
2588
2789
|
const currentRuntime = shared.resolveCcCli(CONFIG.engine);
|
|
@@ -2798,8 +2999,19 @@ function _docChatResultLooksSuccessful(result) {
|
|
|
2798
2999
|
// Build the doc-chat extraContext for a single ccCall pass — refreshed on retry
|
|
2799
3000
|
// so a fresh-session retry includes the full document instead of relying on the
|
|
2800
3001
|
// dead session's prior turn for context.
|
|
2801
|
-
|
|
2802
|
-
|
|
3002
|
+
//
|
|
3003
|
+
// usePool: when true (caller is on the worker-pool path), force `existing=null`
|
|
3004
|
+
// so the docUnchanged optimization never fires. Pool ACP session IDs are valid
|
|
3005
|
+
// only inside the live worker process; when the idle reaper kills the worker
|
|
3006
|
+
// (10 min), the server restarts, or the operator flips ccUseWorkerPool with
|
|
3007
|
+
// prior legacy history on disk, a stale docSessions._docHash matching the
|
|
3008
|
+
// current doc would otherwise emit docUnchanged:true → _formatDocChatContext
|
|
3009
|
+
// replaces the doc body with "unchanged from the previous turn" → the fresh
|
|
3010
|
+
// ACP worker answers from an empty document context. Always re-sending the doc
|
|
3011
|
+
// body on the pool path is the simplest correctness-safe contract; the pool's
|
|
3012
|
+
// warm-process savings are preserved regardless.
|
|
3013
|
+
function _buildDocChatPass({ docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession, usePool }) {
|
|
3014
|
+
const existing = (freshSession || usePool) ? null : resolveSession('doc', sessionKey);
|
|
2803
3015
|
const docHash = require('crypto').createHash('md5').update(docSlice).digest('hex').slice(0, 8);
|
|
2804
3016
|
const docUnchanged = existing?.sessionId && existing._docHash === docHash;
|
|
2805
3017
|
const extraContext = _formatDocChatContext({
|
|
@@ -3006,16 +3218,27 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3006
3218
|
// Skip persistDocSessions() here — the post-call cleanup below handles persistence.
|
|
3007
3219
|
}
|
|
3008
3220
|
|
|
3221
|
+
// Sub-task D of W-mp2w003600196c51: when the worker pool is enabled,
|
|
3222
|
+
// ccCall routes this call through _invokeDocChatViaPool. The pool path
|
|
3223
|
+
// gets a fresh ACP worker on flag flip / reaper / restart, so the
|
|
3224
|
+
// docUnchanged optimization (read side) and the _docHash write-back
|
|
3225
|
+
// (write side) must both be disabled — otherwise the next call silently
|
|
3226
|
+
// ships the "unchanged from the previous turn" sentinel against a worker
|
|
3227
|
+
// that has never seen the doc. See _buildDocChatPass for the read-side
|
|
3228
|
+
// rationale; see the post-call write-back below for the symmetric write
|
|
3229
|
+
// guard.
|
|
3230
|
+
const usePool = !!(CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey);
|
|
3231
|
+
|
|
3009
3232
|
// Build the pass once for the first call; the retry helper invokes runOnce()
|
|
3010
3233
|
// with no args after invalidating the session, so a fresh build there reflects
|
|
3011
3234
|
// the new (no-session) state correctly.
|
|
3012
3235
|
const initialPass = _buildDocChatPass({
|
|
3013
|
-
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
3236
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession, usePool,
|
|
3014
3237
|
});
|
|
3015
3238
|
|
|
3016
3239
|
const runOnce = async (passOverride) => {
|
|
3017
3240
|
const { extraContext } = passOverride || _buildDocChatPass({
|
|
3018
|
-
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
3241
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession, usePool,
|
|
3019
3242
|
});
|
|
3020
3243
|
return ccCall(message, {
|
|
3021
3244
|
store: 'doc', sessionKey,
|
|
@@ -3030,6 +3253,11 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3030
3253
|
turnId,
|
|
3031
3254
|
...(model ? { model } : {}),
|
|
3032
3255
|
onAbortReady,
|
|
3256
|
+
// Sub-task D of W-mp2w003600196c51: forward freshSession so the pool
|
|
3257
|
+
// path can use a one-shot tabKey for one-shot flows (Create Plan
|
|
3258
|
+
// from meeting etc.) — prevents the one-shot from inheriting OR
|
|
3259
|
+
// polluting the persistent doc-chat conversation for the same file.
|
|
3260
|
+
freshSession,
|
|
3033
3261
|
});
|
|
3034
3262
|
};
|
|
3035
3263
|
|
|
@@ -3041,8 +3269,10 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3041
3269
|
// bleed into future interactions under the same key.
|
|
3042
3270
|
docSessions.delete(sessionKey);
|
|
3043
3271
|
schedulePersistDocSessions();
|
|
3044
|
-
} else if (_docChatResultLooksSuccessful(result) && result.sessionId) {
|
|
3045
|
-
// Store doc hash for next call's unchanged check
|
|
3272
|
+
} else if (!usePool && _docChatResultLooksSuccessful(result) && result.sessionId) {
|
|
3273
|
+
// Store doc hash for next call's unchanged check (legacy path only —
|
|
3274
|
+
// pool path skips this so a future call cannot silently ride a stale
|
|
3275
|
+
// hash into the docUnchanged branch against a fresh ACP worker).
|
|
3046
3276
|
const session = resolveSession('doc', sessionKey);
|
|
3047
3277
|
if (session) session._docHash = initialPass.docHash;
|
|
3048
3278
|
}
|
|
@@ -3075,14 +3305,19 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
3075
3305
|
docSessions.delete(sessionKey);
|
|
3076
3306
|
}
|
|
3077
3307
|
|
|
3308
|
+
// Sub-task D of W-mp2w003600196c51: see ccDocCall above for the
|
|
3309
|
+
// read-side / write-side rationale; the streaming variant mirrors the
|
|
3310
|
+
// same usePool guard.
|
|
3311
|
+
const usePool = !!(CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey);
|
|
3312
|
+
|
|
3078
3313
|
// Build the pass once; see ccDocCall for the dedup rationale.
|
|
3079
3314
|
const initialPass = _buildDocChatPass({
|
|
3080
|
-
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
3315
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession, usePool,
|
|
3081
3316
|
});
|
|
3082
3317
|
|
|
3083
3318
|
const runOnce = async (passOverride) => {
|
|
3084
3319
|
const { extraContext } = passOverride || _buildDocChatPass({
|
|
3085
|
-
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
3320
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession, usePool,
|
|
3086
3321
|
});
|
|
3087
3322
|
return ccCallStreaming(message, {
|
|
3088
3323
|
store: 'doc', sessionKey,
|
|
@@ -3099,6 +3334,9 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
3099
3334
|
onChunk: streamStripper,
|
|
3100
3335
|
onToolUse,
|
|
3101
3336
|
onRetry,
|
|
3337
|
+
// Sub-task D of W-mp2w003600196c51: forward freshSession — see
|
|
3338
|
+
// ccDocCall for the one-shot pool-isolation rationale.
|
|
3339
|
+
freshSession,
|
|
3102
3340
|
});
|
|
3103
3341
|
};
|
|
3104
3342
|
|
|
@@ -3108,7 +3346,8 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
3108
3346
|
if (freshSession && sessionKey) {
|
|
3109
3347
|
docSessions.delete(sessionKey);
|
|
3110
3348
|
schedulePersistDocSessions();
|
|
3111
|
-
} else if (_docChatResultLooksSuccessful(result) && result.sessionId) {
|
|
3349
|
+
} else if (!usePool && _docChatResultLooksSuccessful(result) && result.sessionId) {
|
|
3350
|
+
// Legacy path only — see ccDocCall for the pool-path skip rationale.
|
|
3112
3351
|
const session = resolveSession('doc', sessionKey);
|
|
3113
3352
|
if (session) session._docHash = initialPass.docHash;
|
|
3114
3353
|
}
|
|
@@ -8085,6 +8324,9 @@ module.exports = {
|
|
|
8085
8324
|
_normalizeMeetingParticipants: normalizeMeetingParticipants,
|
|
8086
8325
|
parsePinnedEntries,
|
|
8087
8326
|
_formatDocChatContext,
|
|
8327
|
+
_buildDocChatPass,
|
|
8328
|
+
_docSessionsForTesting: docSessions,
|
|
8329
|
+
_docChatPromptHashForTesting: _docChatPromptHash,
|
|
8088
8330
|
_isCompletedMeetingJson,
|
|
8089
8331
|
_finalizeDocChatEdit,
|
|
8090
8332
|
_makeDocChatStreamStripper,
|
package/engine/dispatch.js
CHANGED
|
@@ -176,9 +176,10 @@ function addToDispatch(item) {
|
|
|
176
176
|
|
|
177
177
|
// ─── Pre-Dispatch Acceptance Criteria Gate (P-a2d6b9c7, Ripley §3) ──────────
|
|
178
178
|
//
|
|
179
|
-
//
|
|
179
|
+
// Cheap-LLM validation gate that runs *before* queue insertion so
|
|
180
180
|
// impossible/ambiguous work items are routed to a review queue rather than
|
|
181
|
-
// burning a full agent run.
|
|
181
|
+
// burning a full agent run. On by default via ENGINE_DEFAULTS.enablePreDispatchEval
|
|
182
|
+
// (P-d2a9f6e5); per-project disable available via the dashboard config UI.
|
|
182
183
|
//
|
|
183
184
|
// Wired from engine.js discoverWork(); kept as a separate async wrapper so
|
|
184
185
|
// the existing synchronous addToDispatch() call sites are unaffected.
|
|
@@ -248,7 +249,17 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
248
249
|
|
|
249
250
|
const wi = item?.meta?.item;
|
|
250
251
|
const criteria = wi && (wi.acceptance_criteria || wi.acceptanceCriteria);
|
|
251
|
-
|
|
252
|
+
const hasCriteria = Array.isArray(criteria) && criteria.length > 0;
|
|
253
|
+
// P-d2a9f6e5: also let the validator run when criteria are absent but the
|
|
254
|
+
// work item carries a rich description — defense-in-depth for noop dispatches
|
|
255
|
+
// authored from description-only WIs (same incident class as P-a4f7c2d8).
|
|
256
|
+
// The threshold lives in pre-dispatch-eval.js (DESCRIPTION_MIN_CHARS); we
|
|
257
|
+
// duplicate the bypass check here so we don't even spin up the validator
|
|
258
|
+
// when there's nothing usable to evaluate.
|
|
259
|
+
const { DESCRIPTION_MIN_CHARS } = require('./pre-dispatch-eval');
|
|
260
|
+
const description = ((wi && wi.description) || '').trim();
|
|
261
|
+
const hasUsableDescription = description.length >= DESCRIPTION_MIN_CHARS;
|
|
262
|
+
if (!hasCriteria && !hasUsableDescription) {
|
|
252
263
|
return addToDispatch(item);
|
|
253
264
|
}
|
|
254
265
|
|
|
@@ -454,6 +465,11 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
454
465
|
const agentRetryable = normalizeRetryableDecision(opts.agentRetryable ?? opts.retryable);
|
|
455
466
|
let item = null;
|
|
456
467
|
let completedSourceWorkItem = false;
|
|
468
|
+
// Pre-spawn failures (e.g. worktree creation) move the item from pending →
|
|
469
|
+
// completed without it ever entering active. Track this so the human-feedback
|
|
470
|
+
// dedupe-clear below can skip clearing the completed entry — otherwise the
|
|
471
|
+
// engine immediately re-queues a fix that has no chance of succeeding (#2454).
|
|
472
|
+
let wasPending = false;
|
|
457
473
|
|
|
458
474
|
mutateDispatch((dispatch) => {
|
|
459
475
|
// Check active list first
|
|
@@ -463,7 +479,10 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
463
479
|
} else {
|
|
464
480
|
// Also check pending list (e.g., worktree failure before spawn)
|
|
465
481
|
idx = dispatch.pending.findIndex(d => d.id === id);
|
|
466
|
-
if (idx >= 0)
|
|
482
|
+
if (idx >= 0) {
|
|
483
|
+
item = dispatch.pending.splice(idx, 1)[0];
|
|
484
|
+
wasPending = true;
|
|
485
|
+
}
|
|
467
486
|
}
|
|
468
487
|
|
|
469
488
|
if (!item) return dispatch;
|
|
@@ -625,14 +644,21 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
625
644
|
else log('info', `Skipped pendingFix restore for ${prId} — PR is no longer tracked`);
|
|
626
645
|
} catch (e) { log('warn', `restore pendingFix: ${e.message}`); }
|
|
627
646
|
}
|
|
628
|
-
// Clear completed dispatch entry so dedup doesn't block re-dispatch
|
|
629
|
-
|
|
647
|
+
// Clear completed dispatch entry so dedup doesn't block re-dispatch.
|
|
648
|
+
// SKIP this clear when the failure happened before spawn (e.g. worktree
|
|
649
|
+
// creation failure, #2454) — otherwise the engine sees no recent dispatch
|
|
650
|
+
// history, clears the cooldown as "stale", and re-queues immediately on
|
|
651
|
+
// the very next tick, looping forever for structural failures like a
|
|
652
|
+
// stale-locked worktree entry that the agent can't fix on its own.
|
|
653
|
+
if (item.meta?.dispatchKey && !wasPending) {
|
|
630
654
|
try {
|
|
631
655
|
mutateDispatch((dp) => {
|
|
632
656
|
dp.completed = Array.isArray(dp.completed) ? dp.completed.filter(d => d.meta?.dispatchKey !== item.meta.dispatchKey) : [];
|
|
633
657
|
return dp;
|
|
634
658
|
});
|
|
635
659
|
} catch (e) { log('warn', 'clear human-feedback dispatch for retry: ' + e.message); }
|
|
660
|
+
} else if (item.meta?.dispatchKey && wasPending) {
|
|
661
|
+
log('info', `Preserving completed dispatch entry for ${item.meta.dispatchKey} (pre-spawn failure) — cooldown will throttle retry`);
|
|
636
662
|
}
|
|
637
663
|
}
|
|
638
664
|
}
|
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Conservative scope (per task contract):
|
|
8
8
|
* - Validation only — never rewrites or "fixes" criteria.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
9
|
+
* - Default behavior is governed by `ENGINE_DEFAULTS.enablePreDispatchEval`
|
|
10
|
+
* (P-d2a9f6e5: now `true` by default; per-project override via the
|
|
11
|
+
* dashboard config UI).
|
|
12
|
+
* - When `acceptance_criteria` are absent but the work item carries a rich
|
|
13
|
+
* `description` (≥ DESCRIPTION_MIN_CHARS after trim), the validator
|
|
14
|
+
* evaluates the description instead of fail-open bypassing — so noop
|
|
15
|
+
* dispatches authored from a description-only WI are still caught
|
|
16
|
+
* (P-d2a9f6e5; defense-in-depth for the same incident class as
|
|
17
|
+
* P-a4f7c2d8 dashboard depends_on bug).
|
|
11
18
|
* - Fail-open: any LLM error / runtime-unavailable / parse failure resolves
|
|
12
|
-
* `{ valid: true }` so the gate cannot wedge dispatch
|
|
19
|
+
* `{ valid: true }` so the gate cannot wedge dispatch.
|
|
13
20
|
*
|
|
14
21
|
* Wired from engine/dispatch.js → addToDispatchWithValidation().
|
|
15
22
|
*
|
|
@@ -17,6 +24,8 @@
|
|
|
17
24
|
* knowledge/architecture/2026-05-11-ripley-daily-architecture-bug-review-ripley-s-investigati.md
|
|
18
25
|
* (Daily Architecture & Bug Review — 2026-05-11). Lambert + Rebecca debate
|
|
19
26
|
* rounds reaffirmed: validate-only, no auto-rewrite, opt-in flag.
|
|
27
|
+
* P-d2a9f6e5 (architecture meeting 2026-05-13): flip default to true and
|
|
28
|
+
* broaden validator to read description when criteria are missing.
|
|
20
29
|
*/
|
|
21
30
|
|
|
22
31
|
const shared = require('./shared');
|
|
@@ -26,6 +35,10 @@ const { callLLM } = require('./llm');
|
|
|
26
35
|
const SYSTEM_PROMPT = 'Output only JSON.';
|
|
27
36
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
28
37
|
const DEFAULT_MODEL = 'haiku'; // claude shorthand; the runtime adapter expands it (see engine/runtimes/claude.js resolveModel)
|
|
38
|
+
// Minimum trimmed description length that justifies a description-only LLM
|
|
39
|
+
// validation. Below this we fail-open without burning an LLM call — short
|
|
40
|
+
// descriptions don't carry enough signal for the gate to add value.
|
|
41
|
+
const DESCRIPTION_MIN_CHARS = 80;
|
|
29
42
|
|
|
30
43
|
function _extractCriteria(workItem) {
|
|
31
44
|
if (!workItem || typeof workItem !== 'object') return [];
|
|
@@ -36,18 +49,30 @@ function _extractCriteria(workItem) {
|
|
|
36
49
|
return [];
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
function _extractDescription(workItem) {
|
|
53
|
+
if (!workItem || typeof workItem !== 'object') return '';
|
|
54
|
+
return (workItem.description || '').trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
function _buildPrompt(workItem, criteria) {
|
|
40
58
|
const title = workItem.title || workItem.name || workItem.id || 'untitled';
|
|
41
|
-
const description = (workItem
|
|
59
|
+
const description = _extractDescription(workItem);
|
|
42
60
|
const lines = [
|
|
43
61
|
`Work item: ${title}`,
|
|
44
62
|
];
|
|
63
|
+
const hasCriteria = Array.isArray(criteria) && criteria.length > 0;
|
|
45
64
|
if (description) lines.push('', 'Description:', description);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'
|
|
50
|
-
|
|
65
|
+
if (hasCriteria) {
|
|
66
|
+
lines.push('', 'Acceptance criteria:');
|
|
67
|
+
for (const c of criteria) lines.push(`- ${c}`);
|
|
68
|
+
lines.push('',
|
|
69
|
+
'Are these acceptance criteria clear, actionable, and testable?',
|
|
70
|
+
'Reply with JSON: {"valid": true|false, "reason": "..."}.');
|
|
71
|
+
} else {
|
|
72
|
+
lines.push('',
|
|
73
|
+
'Is this work item description clear, actionable, and testable?',
|
|
74
|
+
'Reply with JSON: {"valid": true|false, "reason": "..."}.');
|
|
75
|
+
}
|
|
51
76
|
return lines.join('\n');
|
|
52
77
|
}
|
|
53
78
|
|
|
@@ -67,6 +92,12 @@ function _parseResponse(text) {
|
|
|
67
92
|
/**
|
|
68
93
|
* Validate a work item's acceptance criteria with a fast/cheap LLM call.
|
|
69
94
|
*
|
|
95
|
+
* Validation modes (P-d2a9f6e5):
|
|
96
|
+
* 1. Criteria present → evaluate the criteria (legacy behavior).
|
|
97
|
+
* 2. No criteria but description ≥ DESCRIPTION_MIN_CHARS → evaluate the
|
|
98
|
+
* description (defense-in-depth for description-only work items).
|
|
99
|
+
* 3. Otherwise → fail-open with `{ valid: true }`.
|
|
100
|
+
*
|
|
70
101
|
* @param {object} workItem - work item with `acceptance_criteria` (or
|
|
71
102
|
* `acceptanceCriteria`) plus title/description for context.
|
|
72
103
|
* @param {object} [opts]
|
|
@@ -78,7 +109,11 @@ function _parseResponse(text) {
|
|
|
78
109
|
*/
|
|
79
110
|
async function validateAcceptanceCriteria(workItem, opts = {}) {
|
|
80
111
|
const criteria = _extractCriteria(workItem);
|
|
81
|
-
|
|
112
|
+
const description = _extractDescription(workItem);
|
|
113
|
+
const hasCriteria = criteria.length > 0;
|
|
114
|
+
const hasUsableDescription = description.length >= DESCRIPTION_MIN_CHARS;
|
|
115
|
+
|
|
116
|
+
if (!hasCriteria && !hasUsableDescription) {
|
|
82
117
|
return { valid: true, reason: 'no acceptance criteria to validate' };
|
|
83
118
|
}
|
|
84
119
|
|
|
@@ -115,9 +150,12 @@ async function validateAcceptanceCriteria(workItem, opts = {}) {
|
|
|
115
150
|
log('warn', 'pre-dispatch-eval: response missing boolean valid field — failing open');
|
|
116
151
|
return { valid: true, reason: 'validator response unparseable' };
|
|
117
152
|
}
|
|
153
|
+
const defaultReason = hasCriteria
|
|
154
|
+
? (parsed.valid ? 'criteria look testable' : 'criteria not clear/actionable/testable')
|
|
155
|
+
: (parsed.valid ? 'description looks testable' : 'description not clear/actionable/testable');
|
|
118
156
|
return {
|
|
119
157
|
valid: parsed.valid,
|
|
120
|
-
reason: String(parsed.reason || '').trim() ||
|
|
158
|
+
reason: String(parsed.reason || '').trim() || defaultReason,
|
|
121
159
|
};
|
|
122
160
|
}
|
|
123
161
|
|
|
@@ -125,6 +163,8 @@ module.exports = {
|
|
|
125
163
|
validateAcceptanceCriteria,
|
|
126
164
|
// Exposed for unit testing — engine code MUST go through validateAcceptanceCriteria.
|
|
127
165
|
_extractCriteria,
|
|
166
|
+
_extractDescription,
|
|
128
167
|
_buildPrompt,
|
|
129
168
|
_parseResponse,
|
|
169
|
+
DESCRIPTION_MIN_CHARS,
|
|
130
170
|
};
|
package/engine/shared.js
CHANGED
|
@@ -1100,7 +1100,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1100
1100
|
botCommentLogins: [], // P-a3f9b2c1: opt-in shared-minions GH login list — comments from these logins are suppressed ONLY when body matches positive-signal markers (Verification SUCCESS / VERDICT:APPROVE / noop:true). Narrower than ignoredCommentAuthors which suppresses all comments by login.
|
|
1101
1101
|
agentBusyReassignMs: 600000, // 10min — reassign work item to another agent if preferred agent is busy beyond this threshold
|
|
1102
1102
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
1103
|
-
enablePreDispatchEval:
|
|
1103
|
+
enablePreDispatchEval: true, // P-d2a9f6e5: cheap LLM gate before queueing — on by default. See engine/pre-dispatch-eval.js (Ripley §3 recommendation, 2026-05-11 architecture review). Validates from acceptance_criteria when present, falls back to description when criteria are absent but description is rich (≥80 chars). Fail-open on any validator error.
|
|
1104
1104
|
|
|
1105
1105
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
1106
1106
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
package/engine.js
CHANGED
|
@@ -573,6 +573,39 @@ async function runWorktreeAdd(rootDir, worktreePath, args, gitOpts, worktreeCrea
|
|
|
573
573
|
if (lastErr) throw lastErr;
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
// Detect and remove worktree registrations whose backing directory is missing
|
|
577
|
+
// on disk. `git worktree add` fails with "branch is already used by worktree
|
|
578
|
+
// at <path>" when a prior crash left such an entry behind, sometimes still in
|
|
579
|
+
// `locked: initializing` state. Single -f errors out with "cannot remove a
|
|
580
|
+
// locked working tree, lock reason: initializing" — must use -f -f.
|
|
581
|
+
// Returns the number of stale entries removed (so callers can decide whether
|
|
582
|
+
// to retry the worktree add).
|
|
583
|
+
async function pruneStaleWorktreeForBranch(rootDir, branchName, gitOpts) {
|
|
584
|
+
if (!branchName) return 0;
|
|
585
|
+
let trees = [];
|
|
586
|
+
try {
|
|
587
|
+
const out = await execAsync(`git worktree list --porcelain`, { ...gitOpts, cwd: rootDir, timeout: 10000 });
|
|
588
|
+
trees = shared.parseWorktreePorcelain(out);
|
|
589
|
+
} catch (e) {
|
|
590
|
+
log('warn', `pruneStaleWorktreeForBranch list: ${e.message?.split('\n')[0]}`);
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
593
|
+
const stale = trees.filter(w => w.branch === branchName && w.path && !fs.existsSync(w.path));
|
|
594
|
+
if (stale.length === 0) return 0;
|
|
595
|
+
let removed = 0;
|
|
596
|
+
for (const w of stale) {
|
|
597
|
+
try {
|
|
598
|
+
await execAsync(`git worktree remove -f -f "${w.path}"`, { ...gitOpts, cwd: rootDir, timeout: 15000 });
|
|
599
|
+
removed++;
|
|
600
|
+
log('warn', `Removed stale worktree entry for ${branchName} at missing path ${w.path}${w.locked ? ' (was locked)' : ''}`);
|
|
601
|
+
} catch (e) {
|
|
602
|
+
log('warn', `git worktree remove -f -f failed for stale ${w.path}: ${e.message?.split('\n')[0]}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
try { await execAsync(`git worktree prune`, { ...gitOpts, cwd: rootDir, timeout: 10000 }); } catch { /* best-effort */ }
|
|
606
|
+
return removed;
|
|
607
|
+
}
|
|
608
|
+
|
|
576
609
|
async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts) {
|
|
577
610
|
if (!branchName) return false;
|
|
578
611
|
const existingWt = await findExistingWorktree(rootDir, branchName);
|
|
@@ -717,7 +750,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
717
750
|
shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
|
|
718
751
|
log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
719
752
|
worktreePath = existingWtPath;
|
|
720
|
-
} else {
|
|
753
|
+
} else {
|
|
754
|
+
// Branch is registered but path is missing on disk (#2454).
|
|
755
|
+
// git keeps the entry — sometimes locked: initializing — and
|
|
756
|
+
// every subsequent add fails identically. Force-prune and retry.
|
|
757
|
+
const pruned = await pruneStaleWorktreeForBranch(rootDir, branchName, _gitOpts);
|
|
758
|
+
if (pruned > 0) {
|
|
759
|
+
log('info', `Pruned ${pruned} stale worktree entry(ies) for shared branch ${branchName}; retrying worktree add`);
|
|
760
|
+
await runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, 0);
|
|
761
|
+
} else { throw eShared; }
|
|
762
|
+
}
|
|
721
763
|
} else if (eShared.message?.includes('invalid reference') || eShared.message?.includes('not a valid ref')) {
|
|
722
764
|
// Branch doesn't exist yet (first item in plan) — create it from main
|
|
723
765
|
const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
@@ -800,9 +842,14 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
800
842
|
cwd = worktreePath;
|
|
801
843
|
log('warn', `Proceeding with recovered worktree after add failure for ${branchName}`);
|
|
802
844
|
} else {
|
|
845
|
+
// Include err.stderr (where git's "fatal: ..." text lives) and use a
|
|
846
|
+
// larger budget than 200 chars — the issue title is unreadable when
|
|
847
|
+
// the slice cuts off mid-"fatal:" leaving just "fa" (#2454).
|
|
848
|
+
const stderrSnippet = err.stderr ? '\n' + err.stderr.toString().trim() : '';
|
|
849
|
+
const fullMsg = `${err.message || ''}${stderrSnippet}`.trim();
|
|
803
850
|
log('error', `Failed to create worktree for ${branchName}: ${err.message}${err.stderr ? '\n' + err.stderr.toString().slice(0, 500) : ''}`);
|
|
804
851
|
_cleanupPromptFiles();
|
|
805
|
-
completeDispatch(id, DISPATCH_RESULT.ERROR, 'Worktree creation failed: ' +
|
|
852
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, 'Worktree creation failed: ' + fullMsg.slice(0, 800));
|
|
806
853
|
cleanupTempAgent(agentId);
|
|
807
854
|
return null;
|
|
808
855
|
}
|
|
@@ -5075,6 +5122,7 @@ module.exports = {
|
|
|
5075
5122
|
reconcileItemsWithPrs, detectDependencyCycles,
|
|
5076
5123
|
parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
|
|
5077
5124
|
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, // exported for testing
|
|
5125
|
+
pruneStaleWorktreeForBranch, // exported for testing
|
|
5078
5126
|
_maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
|
|
5079
5127
|
promoteCheckpointSteeringForClose, // exported for testing
|
|
5080
5128
|
normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1922",
|
|
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"
|