@yemi33/minions 0.1.1618 → 0.1.1620
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/CHANGELOG.md +9 -4
- package/dashboard/js/command-center.js +25 -1
- package/dashboard/js/render-prs.js +5 -3
- package/dashboard/styles.css +1 -0
- package/dashboard.js +101 -42
- package/docs/pr-review-fix-loop.md +9 -4
- package/engine/copilot-models.json +1 -1
- package/engine/llm.js +100 -6
- package/engine/routing.js +41 -8
- package/engine/runtimes/claude.js +1 -0
- package/engine.js +7 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1620 (2026-04-29)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
- progressive Claude token streaming + simplify-pass cleanup
|
|
7
|
+
|
|
8
|
+
## 0.1.1619 (2026-04-29)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- surface partial PR escalation state (#1856)
|
|
12
|
+
|
|
13
|
+
## 0.1.1617 (2026-04-29)
|
|
9
14
|
|
|
10
15
|
### Fixes
|
|
11
16
|
- guard stale build & conflict auto-fixes with live pre-dispatch check (#1851)
|
|
@@ -14,6 +14,22 @@ var _ccSending = false; // true if active tab is sending (UI indicator only)
|
|
|
14
14
|
// Clear stale sending state on page load — SSE streams don't survive refresh
|
|
15
15
|
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
16
16
|
|
|
17
|
+
function _ccStripActionBlockFromText(value) {
|
|
18
|
+
var text = value || '';
|
|
19
|
+
if (!text) return text;
|
|
20
|
+
var full = /(?:^|\r?\n)===ACTIONS={0,3}[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
21
|
+
if (full) return text.slice(0, full.index + full[0].indexOf('===ACTIONS')).trim();
|
|
22
|
+
var block = /(?:^|\r?\n)===ACTIONS\b[^\r\n]*(?=\r?\n|$)/m.exec(text);
|
|
23
|
+
if (block) return text.slice(0, block.index + block[0].indexOf('===ACTIONS')).trim();
|
|
24
|
+
var delimiter = '===ACTIONS===';
|
|
25
|
+
var lineStart = Math.max(text.lastIndexOf('\n'), text.lastIndexOf('\r')) + 1;
|
|
26
|
+
var trailingLine = text.slice(lineStart).trimEnd();
|
|
27
|
+
if (trailingLine.length >= 3 && trailingLine.length < delimiter.length && delimiter.indexOf(trailingLine) === 0) {
|
|
28
|
+
return text.slice(0, lineStart).trimEnd();
|
|
29
|
+
}
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
// ── Migration from legacy single-session format ─────────────────────────────
|
|
18
34
|
(function _ccMigrateLegacy() {
|
|
19
35
|
try {
|
|
@@ -55,8 +71,12 @@ function _ccActiveTab() {
|
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
function _ccMergeStreamText(prev, incoming) {
|
|
74
|
+
// `prev` is already merged-clean from prior frames (server strips actions
|
|
75
|
+
// before SSE emission, and any leaked partial was sanitized by the previous
|
|
76
|
+
// _ccMergeStreamText call). Only strip `incoming` defensively — re-stripping
|
|
77
|
+
// `prev` every frame is O(n²) over the response length for nothing.
|
|
58
78
|
var current = prev || '';
|
|
59
|
-
var next = incoming || '';
|
|
79
|
+
var next = _ccStripActionBlockFromText(incoming || '');
|
|
60
80
|
if (!current) return next;
|
|
61
81
|
if (!next) return current;
|
|
62
82
|
if (next === current) return current;
|
|
@@ -601,6 +621,10 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
601
621
|
updateStreamDiv();
|
|
602
622
|
} else if (evt.type === 'heartbeat') {
|
|
603
623
|
return;
|
|
624
|
+
} else if (evt.type === 'thinking') {
|
|
625
|
+
streamStatusNote = evt.text || 'Thinking...';
|
|
626
|
+
if (activeTab) activeTab._streamStatusNote = streamStatusNote;
|
|
627
|
+
updateStreamDiv();
|
|
604
628
|
} else if (evt.type === 'tool') {
|
|
605
629
|
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
606
630
|
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
@@ -10,8 +10,10 @@ function prRow(pr) {
|
|
|
10
10
|
// If PR is merged/abandoned, treat 'waiting' review as resolved
|
|
11
11
|
const effectiveReviewStatus = (pr.status === 'merged' || pr.status === 'abandoned') && pr.reviewStatus === 'waiting' ? (pr.status === 'merged' ? 'approved' : 'pending') : pr.reviewStatus;
|
|
12
12
|
const reviewSource = sq.status || effectiveReviewStatus || 'pending';
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const reviewEscalated = !!pr._evalEscalated;
|
|
14
|
+
const reviewClass = reviewEscalated ? 'review-escalated' : reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
|
|
15
|
+
const reviewLabel = reviewEscalated ? 'review loop escalated (build/conflict may still run)' : sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
|
|
16
|
+
const reviewTitle = reviewEscalated ? 'Review/re-review and review-fix automation stopped after evalMaxIterations; build-fix and conflict-fix automation may still run.' : '';
|
|
15
17
|
const buildClass = pr.buildFixEscalated ? 'build-escalated' : pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
16
18
|
const buildLabel = pr.buildFixEscalated ? 'escalated (' + (pr.buildFixAttempts || '?') + ' fixes)' : (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
|
|
17
19
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
@@ -23,7 +25,7 @@ function prRow(pr) {
|
|
|
23
25
|
'<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(pr.title || 'Untitled') + '</a>' + (pr.description ? '<div class="pr-desc">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
|
|
24
26
|
'<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
|
|
25
27
|
'<td><span class="pr-branch">' + escapeHtml(pr.branch || '—') + '</span></td>' +
|
|
26
|
-
'<td><span class="pr-badge ' + reviewClass + '">' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
28
|
+
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
27
29
|
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
28
30
|
'<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
|
29
31
|
'<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
package/dashboard/styles.css
CHANGED
|
@@ -268,6 +268,7 @@
|
|
|
268
268
|
.pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
269
269
|
.pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
270
270
|
.pr-badge.needs-review { background: rgba(227,179,65,0.15); color: var(--orange); border: 1px solid var(--orange); }
|
|
271
|
+
.pr-badge.review-escalated { background: rgba(227,179,65,0.22); color: var(--orange); border: 1px dashed var(--orange); font-weight: 700; }
|
|
271
272
|
.pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
|
|
272
273
|
.pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
|
|
273
274
|
.pr-badge.build-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
package/dashboard.js
CHANGED
|
@@ -584,6 +584,7 @@ function _ensureCcLiveStream(tabId) {
|
|
|
584
584
|
tabId,
|
|
585
585
|
text: '',
|
|
586
586
|
tools: [],
|
|
587
|
+
thinking: false,
|
|
587
588
|
donePayload: null,
|
|
588
589
|
writer: null,
|
|
589
590
|
endResponse: null,
|
|
@@ -765,11 +766,54 @@ For all state files, look under \`${MINIONS_DIR}\`.`;
|
|
|
765
766
|
return result;
|
|
766
767
|
}
|
|
767
768
|
|
|
768
|
-
|
|
769
|
+
// Single helper that handles both the strict (well-formed) and loose forms of
|
|
770
|
+
// the ===ACTIONS=== delimiter. `parseable` is true only for the strict form
|
|
771
|
+
// that parseCCActions can JSON.parse; loose matches still split display text
|
|
772
|
+
// but are not parsed (they shouldn't reach the user as actions).
|
|
773
|
+
function findCCActionsHeader(text) {
|
|
774
|
+
if (!text) return null;
|
|
775
|
+
// Strict: ===ACTIONS={0,3} on its own line, optional trailing whitespace.
|
|
776
|
+
const strict = /(?:^|\r?\n)===ACTIONS={0,3}[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
777
|
+
if (strict) {
|
|
778
|
+
const headerStart = strict.index + strict[0].indexOf('===ACTIONS');
|
|
779
|
+
const headerMatch = text.slice(headerStart).match(/^===ACTIONS={0,3}[ \t]*/);
|
|
780
|
+
return {
|
|
781
|
+
index: headerStart,
|
|
782
|
+
headerLength: headerMatch ? headerMatch[0].length : '===ACTIONS==='.length,
|
|
783
|
+
parseable: true,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// Loose: any ===ACTIONS<word-boundary>... line. Catches malformed delimiters
|
|
787
|
+
// like ===ACTIONS -> that should still be hidden from output.
|
|
788
|
+
const loose = /(?:^|\r?\n)===ACTIONS\b[^\r\n]*(?=\r?\n|$)/m.exec(text);
|
|
789
|
+
if (loose) {
|
|
790
|
+
const headerStart = loose.index + loose[0].indexOf('===ACTIONS');
|
|
791
|
+
return { index: headerStart, headerLength: 0, parseable: false };
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function findCCActionsPartialDelimiter(text) {
|
|
769
797
|
if (!text) return -1;
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
798
|
+
const delimiter = '===ACTIONS===';
|
|
799
|
+
const lineStart = Math.max(text.lastIndexOf('\n'), text.lastIndexOf('\r')) + 1;
|
|
800
|
+
const trailingLine = text.slice(lineStart).trimEnd();
|
|
801
|
+
if (trailingLine.length >= 3 && trailingLine.length < delimiter.length && delimiter.startsWith(trailingLine)) {
|
|
802
|
+
return lineStart;
|
|
803
|
+
}
|
|
804
|
+
return -1;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function stripCCActionsForStream(text) {
|
|
808
|
+
if (!text) return '';
|
|
809
|
+
// Fast path: 95% of streamed chunks contain no '=' before any actions block
|
|
810
|
+
// appears. Skip all regex work in that case.
|
|
811
|
+
if (text.indexOf('===') < 0) return text;
|
|
812
|
+
const header = findCCActionsHeader(text);
|
|
813
|
+
if (header) return text.slice(0, header.index).trim();
|
|
814
|
+
const partialIdx = findCCActionsPartialDelimiter(text);
|
|
815
|
+
if (partialIdx >= 0) return text.slice(0, partialIdx).trimEnd();
|
|
816
|
+
return text;
|
|
773
817
|
}
|
|
774
818
|
|
|
775
819
|
// Issue #1834: non-Claude runtimes (Copilot/GPT) routinely wrap the action JSON
|
|
@@ -819,20 +863,23 @@ function parseCCActions(text) {
|
|
|
819
863
|
let actions = [];
|
|
820
864
|
let displayText = text;
|
|
821
865
|
let parseError = null;
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
866
|
+
const header = findCCActionsHeader(text);
|
|
867
|
+
let segment = '';
|
|
868
|
+
if (header) {
|
|
869
|
+
displayText = text.slice(0, header.index).trim();
|
|
870
|
+
if (header.parseable) {
|
|
871
|
+
segment = text.slice(header.index + header.headerLength);
|
|
872
|
+
const jsonStr = _extractActionsJson(segment);
|
|
873
|
+
if (jsonStr) {
|
|
874
|
+
try {
|
|
875
|
+
const parsed = JSON.parse(jsonStr);
|
|
876
|
+
actions = Array.isArray(parsed) ? parsed : [parsed];
|
|
877
|
+
} catch (e) {
|
|
878
|
+
parseError = e.message || 'invalid JSON';
|
|
879
|
+
}
|
|
880
|
+
} else if (segment.trim()) {
|
|
881
|
+
parseError = 'no JSON value found after ===ACTIONS=== delimiter';
|
|
833
882
|
}
|
|
834
|
-
} else if (segment.trim()) {
|
|
835
|
-
parseError = 'no JSON value found after ===ACTIONS=== delimiter';
|
|
836
883
|
}
|
|
837
884
|
}
|
|
838
885
|
if (actions.length === 0) {
|
|
@@ -851,7 +898,7 @@ function parseCCActions(text) {
|
|
|
851
898
|
result._actionParseError = parseError;
|
|
852
899
|
// Visibility for the engine log — silent failure here previously masked issue #1834.
|
|
853
900
|
try {
|
|
854
|
-
const snippet = (
|
|
901
|
+
const snippet = (segment.trim() || '').slice(0, 200);
|
|
855
902
|
console.warn(`[CC] action JSON parse failed (${parseError}); raw segment: ${snippet}`);
|
|
856
903
|
if (typeof shared !== 'undefined' && shared && typeof shared.log === 'function') {
|
|
857
904
|
shared.log('warn', `CC action JSON parse failed: ${parseError} — segment: ${snippet}`);
|
|
@@ -864,8 +911,8 @@ function parseCCActions(text) {
|
|
|
864
911
|
function stripCCActionSyntax(text) {
|
|
865
912
|
if (!text) return '';
|
|
866
913
|
let displayText = text;
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
914
|
+
const header = findCCActionsHeader(text);
|
|
915
|
+
if (header) displayText = text.slice(0, header.index).trim();
|
|
869
916
|
return displayText.replace(/`{3,}\s*action\s*\r?\n[\s\S]*?`{3,}\n?/g, '').trim();
|
|
870
917
|
}
|
|
871
918
|
|
|
@@ -4532,11 +4579,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4532
4579
|
});
|
|
4533
4580
|
}
|
|
4534
4581
|
|
|
4535
|
-
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
4536
|
-
if (!tabId.startsWith('teams-')) {
|
|
4537
|
-
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
4538
|
-
}
|
|
4539
|
-
|
|
4540
4582
|
const parsed = parseCCActions(result.text);
|
|
4541
4583
|
const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
|
|
4542
4584
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
@@ -4549,6 +4591,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4549
4591
|
if (parsed.actions.length > 0) {
|
|
4550
4592
|
parsed.actionResults = await executeCCActions(parsed.actions);
|
|
4551
4593
|
}
|
|
4594
|
+
// Mirror only user-facing text to Teams; never send the internal action block.
|
|
4595
|
+
if (!tabId.startsWith('teams-')) {
|
|
4596
|
+
teams.teamsPostCCResponse(body.message, parsed.text).catch(() => {});
|
|
4597
|
+
}
|
|
4552
4598
|
// Issue #1834: rename _actionParseError → actionParseError (public field)
|
|
4553
4599
|
// so the client can surface a warning when the model emitted ===ACTIONS===
|
|
4554
4600
|
// but the JSON couldn't be recovered.
|
|
@@ -4642,6 +4688,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4642
4688
|
for (const tool of live.tools || []) {
|
|
4643
4689
|
writeCcEvent({ type: 'tool', name: tool.name, input: _lightToolInput(tool.input) });
|
|
4644
4690
|
}
|
|
4691
|
+
if (live.thinking && !live.text) writeCcEvent({ type: 'thinking', text: 'Thinking...' });
|
|
4645
4692
|
if (live.text) writeCcEvent({ type: 'chunk', text: live.text });
|
|
4646
4693
|
if (live.donePayload) {
|
|
4647
4694
|
writeCcEvent(live.donePayload);
|
|
@@ -4723,15 +4770,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4723
4770
|
sessionId, effort: streamEffort, direct: true,
|
|
4724
4771
|
engineConfig: CONFIG.engine,
|
|
4725
4772
|
onChunk: (text) => {
|
|
4726
|
-
const
|
|
4727
|
-
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
4773
|
+
const display = stripCCActionsForStream(text);
|
|
4728
4774
|
liveState.text = display;
|
|
4729
4775
|
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4776
|
+
// Once text is flowing, the SSE-replay branch (live.thinking &&
|
|
4777
|
+
// !live.text) shouldn't show stale "Thinking…" on reconnect.
|
|
4778
|
+
if (liveState.thinking) liveState.thinking = false;
|
|
4730
4779
|
},
|
|
4731
4780
|
onToolUse: (name, input) => {
|
|
4732
4781
|
toolUses.push({ name, input: input || {} });
|
|
4733
4782
|
liveState.tools.push({ name, input: input || {} });
|
|
4734
4783
|
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4784
|
+
},
|
|
4785
|
+
onThinking: () => {
|
|
4786
|
+
liveState.thinking = true;
|
|
4787
|
+
if (liveState.writer) liveState.writer({ type: 'thinking', text: 'Thinking...' });
|
|
4735
4788
|
}
|
|
4736
4789
|
});
|
|
4737
4790
|
_ccStreamAbort = llmPromise.abort;
|
|
@@ -4749,20 +4802,26 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4749
4802
|
toolUses = []; // discard stale metadata from the failed resume attempt
|
|
4750
4803
|
const retryPromise = callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
4751
4804
|
timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
}
|
|
4805
|
+
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
4806
|
+
effort: streamEffort, direct: true,
|
|
4807
|
+
engineConfig: CONFIG.engine,
|
|
4808
|
+
onChunk: (text) => {
|
|
4809
|
+
const display = stripCCActionsForStream(text);
|
|
4810
|
+
liveState.text = display;
|
|
4811
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4812
|
+
// Same reset as the initial path so resume-fail retries don't
|
|
4813
|
+
// leave a stale "Thinking…" frame visible on SSE reconnect.
|
|
4814
|
+
if (liveState.thinking) liveState.thinking = false;
|
|
4815
|
+
},
|
|
4816
|
+
onToolUse: (name, input) => {
|
|
4817
|
+
toolUses.push({ name, input: input || {} });
|
|
4818
|
+
liveState.tools.push({ name, input: input || {} });
|
|
4819
|
+
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4820
|
+
},
|
|
4821
|
+
onThinking: () => {
|
|
4822
|
+
liveState.thinking = true;
|
|
4823
|
+
if (liveState.writer) liveState.writer({ type: 'thinking', text: 'Thinking...' });
|
|
4824
|
+
}
|
|
4766
4825
|
});
|
|
4767
4826
|
_ccStreamAbort = retryPromise.abort;
|
|
4768
4827
|
liveState.abortFn = _ccStreamAbort;
|
|
@@ -4834,7 +4893,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4834
4893
|
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
4835
4894
|
const _streamTabId = body.tabId || 'default';
|
|
4836
4895
|
if (!_streamTabId.startsWith('teams-')) {
|
|
4837
|
-
teams.teamsPostCCResponse(body.message,
|
|
4896
|
+
teams.teamsPostCCResponse(body.message, displayText).catch(() => {});
|
|
4838
4897
|
}
|
|
4839
4898
|
|
|
4840
4899
|
if (liveState.endResponse) liveState.endResponse();
|
|
@@ -34,10 +34,11 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
|
|
|
34
34
|
|
|
35
35
|
### A. Review feedback (`changes-requested`)
|
|
36
36
|
|
|
37
|
-
- Gate: `reviewStatus === 'changes-requested'` + `!awaitingReReview` + not dispatched + not on cooldown
|
|
37
|
+
- Gate: `reviewStatus === 'changes-requested'` + `!awaitingReReview` + `!evalEscalated` + not dispatched + not on cooldown
|
|
38
38
|
- Routes to PR author via `_author_` routing token
|
|
39
39
|
- `review_note` = reviewer's feedback
|
|
40
40
|
- Sets `fixDispatched = true` — prevents human-feedback and conflict fixes from also firing this pass
|
|
41
|
+
- **Review-loop escalation**: after `evalMaxIterations` review→fix cycles (default 3), `_evalEscalated` is set on the PR and *only this trigger plus review/re-review* stop. Triggers B (human comments), C (build failures), and the merge-conflict fix path keep running. The dashboard PR row distinguishes the two states with separate badges (review badge `review-escalated` vs. build badge `build-escalated`).
|
|
41
42
|
|
|
42
43
|
### B. Human comments (`humanFeedback.pendingFix`)
|
|
43
44
|
|
|
@@ -45,13 +46,15 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
|
|
|
45
46
|
- Agent comments filtered out via `/\bMinions\s*\(/i` regex on comment body
|
|
46
47
|
- Coalesces multiple comments arriving during cooldown into single fix
|
|
47
48
|
- Routes to author
|
|
49
|
+
- Not gated by `_evalEscalated` — humans can always force more fixes via PR comments even after the review loop escalates.
|
|
48
50
|
|
|
49
51
|
### C. Build failures (`buildStatus === 'failing'`)
|
|
50
52
|
|
|
51
53
|
- Gate: `buildFixAttempts < maxBuildFixAttempts` (default 3) + grace period expired
|
|
52
54
|
- **Grace period** (`_buildFixPushedAt`): after fix dispatches, waits `buildFixGracePeriod` (default 10min, configurable in `ENGINE_DEFAULTS`) for CI to run before re-dispatching. Cleared when poller detects build status transition (CI actually ran).
|
|
53
55
|
- **Error logs**: GitHub fetches annotations (failures only, not warnings) + Actions job log (always). ADO queries builds API directly (not status checks), fetches build timeline → failed task logs (up to 10 per build, up to 10 failing pipelines).
|
|
54
|
-
- **
|
|
56
|
+
- **Build-fix escalation**: after 3 failed attempts, writes an inbox alert, sets `buildFixEscalated = true`, and stops *only this trigger* (auto-dispatch for build fixes). The counter resets when the build recovers. Independent of `_evalEscalated`.
|
|
57
|
+
- Not gated by `_evalEscalated` — build-fix is mechanical and runs even if the review loop has escalated.
|
|
55
58
|
- Sets `fixDispatched = true` after dispatch so the later conflict trigger is suppressed in the same pass.
|
|
56
59
|
|
|
57
60
|
### D. Merge conflicts (`_mergeConflict`)
|
|
@@ -116,8 +119,10 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
|
|
|
116
119
|
| `reviewStatus` | Poller + post-completion | `pending` / `approved` / `changes-requested` / `waiting` |
|
|
117
120
|
| `buildStatus` | Poller | `none` / `passing` / `failing` / `running` |
|
|
118
121
|
| `buildErrorLog` | Poller | Actual CI error output for fix agents |
|
|
119
|
-
| `buildFixAttempts` | Discovery (on dispatch) | Counter for escalation cap |
|
|
120
|
-
| `buildFixEscalated` | Discovery (on cap) | Stops auto-dispatch |
|
|
122
|
+
| `buildFixAttempts` | Discovery (on dispatch) | Counter for build-fix escalation cap |
|
|
123
|
+
| `buildFixEscalated` | Discovery (on cap) | Stops *build-fix* auto-dispatch only (review/re-review and other fix triggers continue) |
|
|
124
|
+
| `_reviewFixCycles` | Discovery (on dispatch) | Counter for review→fix cycle cap (`evalMaxIterations`) |
|
|
125
|
+
| `_evalEscalated` | Discovery (on cap) | Stops *review/re-review and review-feedback fix* auto-dispatch only (build-fix, conflict-fix, and human-feedback fix continue). Cleared when reviewer eventually approves the PR. |
|
|
121
126
|
| `_buildFixPushedAt` | Discovery (on dispatch) | Grace period timestamp |
|
|
122
127
|
| `_buildFailNotified` | Discovery | Dedup for inbox alert |
|
|
123
128
|
| `lastPushedAt` | Poller (new commit) | Tracks latest push for re-review logic |
|
package/engine/llm.js
CHANGED
|
@@ -24,6 +24,10 @@ const MINIONS_DIR = shared.MINIONS_DIR;
|
|
|
24
24
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
25
25
|
const COPILOT_TASK_COMPLETE_GRACE_MS = 3000;
|
|
26
26
|
|
|
27
|
+
// Claude content blocks come in two thinking variants; hoisted to module scope
|
|
28
|
+
// so the streaming accumulator's hot path doesn't recreate the set per event.
|
|
29
|
+
const THINKING_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking']);
|
|
30
|
+
|
|
27
31
|
// ─── Engine-Usage Metrics ────────────────────────────────────────────────────
|
|
28
32
|
|
|
29
33
|
function trackEngineUsage(category, usage) {
|
|
@@ -261,6 +265,7 @@ function _createStreamAccumulator({
|
|
|
261
265
|
onChunk = null,
|
|
262
266
|
onToolUse = null,
|
|
263
267
|
onTaskComplete = null,
|
|
268
|
+
onThinking = null,
|
|
264
269
|
}) {
|
|
265
270
|
let stdout = '';
|
|
266
271
|
let stderr = '';
|
|
@@ -278,9 +283,15 @@ function _createStreamAccumulator({
|
|
|
278
283
|
let copilotMessageBuffer = '';
|
|
279
284
|
let copilotTaskCompleteSeen = false;
|
|
280
285
|
let copilotTaskCompleteSummary = '';
|
|
286
|
+
const claudeStreamBlocks = new Map();
|
|
287
|
+
// Maintained accumulator of Claude text — incrementally appended on each
|
|
288
|
+
// text_delta so the hot path doesn't rebuild from the Map every chunk
|
|
289
|
+
// (rebuild was O(n) per delta → O(n²) over the response).
|
|
290
|
+
let claudeJoinedText = '';
|
|
291
|
+
let thinkingSent = false;
|
|
281
292
|
|
|
282
293
|
function _streamText(value) {
|
|
283
|
-
return maxTextLength ? value.slice(-maxTextLength) : value;
|
|
294
|
+
return (maxTextLength && value.length > maxTextLength) ? value.slice(-maxTextLength) : value;
|
|
284
295
|
}
|
|
285
296
|
|
|
286
297
|
function _copilotAssistantMessageHasTools(obj) {
|
|
@@ -288,6 +299,79 @@ function _createStreamAccumulator({
|
|
|
288
299
|
return Array.isArray(requests) && requests.length > 0;
|
|
289
300
|
}
|
|
290
301
|
|
|
302
|
+
function _notifyThinking() {
|
|
303
|
+
if (!onThinking || thinkingSent) return;
|
|
304
|
+
thinkingSent = true;
|
|
305
|
+
onThinking();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Rebuild the joined text from the Map. Only used as a safety net when
|
|
309
|
+
// content blocks arrive out of order (a non-trailing index lands after a
|
|
310
|
+
// later one — rare but possible if events get reordered upstream).
|
|
311
|
+
function _rebuildClaudeJoinedText() {
|
|
312
|
+
claudeJoinedText = Array.from(claudeStreamBlocks.keys()).sort((a, b) => a - b)
|
|
313
|
+
.map(index => claudeStreamBlocks.get(index))
|
|
314
|
+
.filter(block => block && block.type === 'text' && block.text)
|
|
315
|
+
.map(block => block.text)
|
|
316
|
+
.join('');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function _captureClaudeText(value) {
|
|
320
|
+
if (typeof value !== 'string' || !value) return;
|
|
321
|
+
const nextText = _streamText(value);
|
|
322
|
+
text = nextText;
|
|
323
|
+
if (onChunk && nextText !== lastTextSent) {
|
|
324
|
+
lastTextSent = nextText;
|
|
325
|
+
onChunk(nextText);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function _captureClaudeStreamEvent(obj) {
|
|
330
|
+
const event = obj?.event;
|
|
331
|
+
if (!event || typeof event !== 'object') return false;
|
|
332
|
+
if (event.type === 'message_start') {
|
|
333
|
+
claudeStreamBlocks.clear();
|
|
334
|
+
claudeJoinedText = '';
|
|
335
|
+
thinkingSent = false;
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
if (event.type === 'content_block_start') {
|
|
339
|
+
const index = Number.isInteger(event.index) ? event.index : Number(event.index) || 0;
|
|
340
|
+
const block = event.content_block || {};
|
|
341
|
+
claudeStreamBlocks.set(index, { type: block.type || '', text: block.text || '' });
|
|
342
|
+
if (THINKING_BLOCK_TYPES.has(block.type)) _notifyThinking();
|
|
343
|
+
// If a block lands at a non-trailing index (out-of-order delivery), the
|
|
344
|
+
// monotonic-append path can't reconstruct the joined text — rebuild as
|
|
345
|
+
// a safety net. The common case is in-order arrival; rebuild is rare.
|
|
346
|
+
const indices = Array.from(claudeStreamBlocks.keys());
|
|
347
|
+
const isTrailing = indices.every(i => i <= index);
|
|
348
|
+
if (!isTrailing) {
|
|
349
|
+
_rebuildClaudeJoinedText();
|
|
350
|
+
} else if (block.type === 'text' && block.text) {
|
|
351
|
+
claudeJoinedText += block.text;
|
|
352
|
+
}
|
|
353
|
+
if (claudeJoinedText) _captureClaudeText(claudeJoinedText);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
if (event.type === 'content_block_delta') {
|
|
357
|
+
const index = Number.isInteger(event.index) ? event.index : Number(event.index) || 0;
|
|
358
|
+
const delta = event.delta || {};
|
|
359
|
+
if (delta.type === 'thinking_delta' || typeof delta.thinking === 'string') _notifyThinking();
|
|
360
|
+
if (delta.type === 'text_delta' && typeof delta.text === 'string' && delta.text) {
|
|
361
|
+
const block = claudeStreamBlocks.get(index) || { type: 'text', text: '' };
|
|
362
|
+
block.type = 'text';
|
|
363
|
+
block.text = (block.text || '') + delta.text;
|
|
364
|
+
claudeStreamBlocks.set(index, block);
|
|
365
|
+
// Common case: deltas arrive monotonically per index, so appending to
|
|
366
|
+
// the joined accumulator directly is correct.
|
|
367
|
+
claudeJoinedText += delta.text;
|
|
368
|
+
_captureClaudeText(claudeJoinedText);
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return event.type === 'content_block_stop' || event.type === 'message_delta' || event.type === 'message_stop';
|
|
373
|
+
}
|
|
374
|
+
|
|
291
375
|
function _captureCopilotTaskComplete(summary, success = true) {
|
|
292
376
|
if (typeof summary !== 'string' || !summary) return;
|
|
293
377
|
const finalSummary = _streamText(summary);
|
|
@@ -311,6 +395,9 @@ function _createStreamAccumulator({
|
|
|
311
395
|
|
|
312
396
|
// ── Claude shape ────────────────────────────────────────────────────────
|
|
313
397
|
if (obj.session_id) sessionId = obj.session_id;
|
|
398
|
+
if (obj.type === 'stream_event') {
|
|
399
|
+
_captureClaudeStreamEvent(obj);
|
|
400
|
+
}
|
|
314
401
|
if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
315
402
|
// Claude result event: terminal text + usage.
|
|
316
403
|
text = maxTextLength ? obj.result.slice(-maxTextLength) : obj.result;
|
|
@@ -328,19 +415,22 @@ function _createStreamAccumulator({
|
|
|
328
415
|
}
|
|
329
416
|
if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
|
|
330
417
|
// Claude assistant turn: content blocks (text + tool_use).
|
|
418
|
+
// Multi-text-block messages (common with --include-partial-messages) need
|
|
419
|
+
// their text joined before _captureClaudeText, otherwise each block
|
|
420
|
+
// overwrites the prior one.
|
|
421
|
+
let assistantText = '';
|
|
331
422
|
for (const block of obj.message.content) {
|
|
332
423
|
if (block?.type === 'text' && block.text) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
onChunk(block.text);
|
|
337
|
-
}
|
|
424
|
+
assistantText += block.text;
|
|
425
|
+
} else if (THINKING_BLOCK_TYPES.has(block?.type)) {
|
|
426
|
+
_notifyThinking();
|
|
338
427
|
} else if (block?.type === 'tool_use' && block.name) {
|
|
339
428
|
const toolUse = { name: block.name, input: block.input || {} };
|
|
340
429
|
toolUses.push(toolUse);
|
|
341
430
|
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
342
431
|
}
|
|
343
432
|
}
|
|
433
|
+
if (assistantText) _captureClaudeText(assistantText);
|
|
344
434
|
}
|
|
345
435
|
|
|
346
436
|
// ── Copilot shape ───────────────────────────────────────────────────────
|
|
@@ -348,6 +438,9 @@ function _createStreamAccumulator({
|
|
|
348
438
|
if (obj.type === 'session.task_complete') {
|
|
349
439
|
_captureCopilotTaskComplete(obj.data?.summary, obj.data?.success);
|
|
350
440
|
}
|
|
441
|
+
if (obj.type === 'assistant.reasoning' || obj.type === 'assistant.reasoning_delta') {
|
|
442
|
+
_notifyThinking();
|
|
443
|
+
}
|
|
351
444
|
if (obj.type === 'assistant.message_delta' && typeof obj.data?.deltaContent === 'string') {
|
|
352
445
|
if (copilotTaskCompleteSeen) return;
|
|
353
446
|
copilotMessageBuffer += obj.data.deltaContent;
|
|
@@ -611,6 +704,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
611
704
|
onChunk,
|
|
612
705
|
onToolUse,
|
|
613
706
|
onTaskComplete: scheduleTaskCompleteClose,
|
|
707
|
+
onThinking: opts.onThinking || null,
|
|
614
708
|
});
|
|
615
709
|
|
|
616
710
|
_abort = () => { shared.killImmediate(proc); };
|
package/engine/routing.js
CHANGED
|
@@ -28,7 +28,7 @@ let _routingCache = null;
|
|
|
28
28
|
let _routingCacheMtime = 0;
|
|
29
29
|
|
|
30
30
|
function parseRoutingTable() {
|
|
31
|
-
const content = getRouting();
|
|
31
|
+
const content = getRouting() || '';
|
|
32
32
|
const routes = {};
|
|
33
33
|
const lines = content.split('\n');
|
|
34
34
|
let inTable = false;
|
|
@@ -116,12 +116,44 @@ function setTempBudget(n) {
|
|
|
116
116
|
}
|
|
117
117
|
function getTempBudget() { return _tempBudget; }
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
// Centralizes the work-item shape used to derive routing hints. Engine code
|
|
120
|
+
// previously inlined `item.preferred_agent || item.agents || null` at four
|
|
121
|
+
// call sites; hoisting keeps the contract in one place.
|
|
122
|
+
function extractAgentHints(item) {
|
|
123
|
+
if (!item || typeof item !== 'object') return null;
|
|
124
|
+
return item.preferred_agent || item.agents || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Normalize a list of agent-hint inputs. Accepts:
|
|
128
|
+
// - Comma-separated string ("dallas,ripley")
|
|
129
|
+
// - Array of strings
|
|
130
|
+
// - Single string
|
|
131
|
+
// Resolves the `_author_` token to authorAgent (when provided), validates
|
|
132
|
+
// each hint against the configured agents map (case-insensitive lookup,
|
|
133
|
+
// returning the canonical ID), dedups, and drops anything unknown.
|
|
134
|
+
function normalizeAgentHints(agentHints, authorAgent = null, agents = null) {
|
|
135
|
+
const raw = Array.isArray(agentHints)
|
|
136
|
+
? agentHints
|
|
137
|
+
: (agentHints ? String(agentHints).split(',') : []);
|
|
138
|
+
const expanded = raw
|
|
139
|
+
.map(id => String(id).trim())
|
|
140
|
+
.map(id => id.toLowerCase() === '_author_' && authorAgent ? String(authorAgent).trim() : id)
|
|
124
141
|
.filter(Boolean);
|
|
142
|
+
// When no agents map is supplied, return lowercased IDs (legacy behaviour
|
|
143
|
+
// used by tests and pre-validation callers).
|
|
144
|
+
if (!agents || typeof agents !== 'object') {
|
|
145
|
+
return expanded.map(id => id.toLowerCase());
|
|
146
|
+
}
|
|
147
|
+
const byLower = new Map(Object.keys(agents).map(id => [id.toLowerCase(), id]));
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const normalized = [];
|
|
150
|
+
for (const hint of expanded) {
|
|
151
|
+
const id = byLower.get(hint.toLowerCase());
|
|
152
|
+
if (!id || seen.has(id)) continue;
|
|
153
|
+
seen.add(id);
|
|
154
|
+
normalized.push(id);
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
function resolveAgent(workType, config, authorAgent = null, agentHints = null) {
|
|
@@ -153,7 +185,7 @@ function resolveAgent(workType, config, authorAgent = null, agentHints = null) {
|
|
|
153
185
|
return null;
|
|
154
186
|
};
|
|
155
187
|
|
|
156
|
-
const hintedAgents = normalizeAgentHints(agentHints, authorAgent);
|
|
188
|
+
const hintedAgents = normalizeAgentHints(agentHints, authorAgent, agents);
|
|
157
189
|
if (hintedAgents.length > 0) {
|
|
158
190
|
for (const id of hintedAgents) {
|
|
159
191
|
if (isAvailable(id)) { _claimedAgents.add(id); return id; }
|
|
@@ -203,10 +235,11 @@ module.exports = {
|
|
|
203
235
|
getMonthlySpend,
|
|
204
236
|
getAgentErrorRate,
|
|
205
237
|
isAgentIdle,
|
|
238
|
+
normalizeAgentHints,
|
|
239
|
+
extractAgentHints,
|
|
206
240
|
_claimedAgents,
|
|
207
241
|
resetClaimedAgents,
|
|
208
242
|
resolveAgent,
|
|
209
243
|
setTempBudget,
|
|
210
244
|
getTempBudget,
|
|
211
|
-
normalizeAgentHints,
|
|
212
245
|
};
|
|
@@ -209,6 +209,7 @@ function buildArgs(opts = {}) {
|
|
|
209
209
|
} = opts;
|
|
210
210
|
|
|
211
211
|
const args = ['-p', '--output-format', outputFormat];
|
|
212
|
+
if (outputFormat === 'stream-json') args.push('--include-partial-messages');
|
|
212
213
|
if (maxTurns != null) args.push('--max-turns', String(maxTurns));
|
|
213
214
|
if (model) args.push('--model', String(model));
|
|
214
215
|
if (verbose) args.push('--verbose');
|
package/engine.js
CHANGED
|
@@ -121,9 +121,10 @@ const { getConfig, getControl, getDispatch, getNotes,
|
|
|
121
121
|
|
|
122
122
|
// ─── Routing (extracted to engine/routing.js) ───────────────────────────────
|
|
123
123
|
|
|
124
|
+
const routing = require('./engine/routing');
|
|
124
125
|
const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
|
|
125
126
|
getAgentErrorRate, isAgentIdle, resolveAgent, resetClaimedAgents,
|
|
126
|
-
setTempBudget, tempAgents } =
|
|
127
|
+
setTempBudget, tempAgents } = routing;
|
|
127
128
|
|
|
128
129
|
// ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
|
|
129
130
|
|
|
@@ -2081,7 +2082,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2081
2082
|
if (target) target._evalEscalated = true;
|
|
2082
2083
|
});
|
|
2083
2084
|
} catch (e) { log('warn', 'mark eval escalated: ' + e.message); }
|
|
2084
|
-
log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending
|
|
2085
|
+
log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending review/re-review and review-fix dispatch; build/conflict fixes may continue`);
|
|
2085
2086
|
}
|
|
2086
2087
|
|
|
2087
2088
|
// PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
|
|
@@ -2500,7 +2501,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2500
2501
|
item._decomposing = true;
|
|
2501
2502
|
needsWrite = true;
|
|
2502
2503
|
}
|
|
2503
|
-
const agentHints =
|
|
2504
|
+
const agentHints = routing.extractAgentHints(item);
|
|
2504
2505
|
const agentId = item.agent || resolveAgent(workType, config, null, agentHints);
|
|
2505
2506
|
if (!agentId) {
|
|
2506
2507
|
// Check if reason is budget
|
|
@@ -3020,7 +3021,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3020
3021
|
|
|
3021
3022
|
} else {
|
|
3022
3023
|
// ─── Normal: single agent dispatch ──────────────────────────────
|
|
3023
|
-
const agentHints =
|
|
3024
|
+
const agentHints = routing.extractAgentHints(item);
|
|
3024
3025
|
const agentId = item.agent || resolveAgent(workType, config, null, agentHints);
|
|
3025
3026
|
if (!agentId) continue;
|
|
3026
3027
|
|
|
@@ -3663,8 +3664,7 @@ async function tickInner() {
|
|
|
3663
3664
|
// be of type string. Received undefined` and re-queues — every tick. Try to
|
|
3664
3665
|
// resolve a fallback via routing; if none is available, skip this tick.
|
|
3665
3666
|
if (!item.agent || typeof item.agent !== 'string') {
|
|
3666
|
-
const
|
|
3667
|
-
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config, null, agentHints);
|
|
3667
|
+
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config, null, routing.extractAgentHints(item.meta?.item));
|
|
3668
3668
|
if (!fallback) {
|
|
3669
3669
|
log('warn', `Pending dispatch ${item.id} has no agent and routing returned no fallback — skipping`);
|
|
3670
3670
|
continue;
|
|
@@ -3722,7 +3722,7 @@ async function tickInner() {
|
|
|
3722
3722
|
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3723
3723
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3724
3724
|
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
3725
|
-
const isExplicitReassign = !!(item.meta?.item?.agent ||
|
|
3725
|
+
const isExplicitReassign = !!(item.meta?.item?.agent || routing.extractAgentHints(item.meta?.item));
|
|
3726
3726
|
if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
|
|
3727
3727
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
|
3728
3728
|
if (Date.now() - busySinceMs > reassignMs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1620",
|
|
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"
|