@goodandready/dsh-moa 0.2.67 → 0.2.69
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/lib/client.js +69 -0
- package/lib/index.js +12 -0
- package/lib/moa-adaptive-personas.js +176 -0
- package/lib/moa-candidates.js +5 -4
- package/lib/moa-parser.js +10 -1
- package/lib/moa-quota-balancer.js +188 -0
- package/lib/moa-schema.js +12 -0
- package/lib/moa-self-healing.js +258 -0
- package/lib/moa-swarm.js +219 -0
- package/lib/moa-workbench.js +12 -0
- package/lib/pipeline/candidates.js +11 -2
- package/lib/pipeline/context.js +20 -2
- package/lib/pipeline/promotion.js +23 -1
- package/lib/pipeline/runner.js +2 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -2017,6 +2017,7 @@ window.__ModuleLoader__.load({
|
|
|
2017
2017
|
t("aggregator.report_desc") || "Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown."
|
|
2018
2018
|
)
|
|
2019
2019
|
),
|
|
2020
|
+
// --- src/client/85-card-aggregator-ext.js ---
|
|
2020
2021
|
/* Interactive Workbench & File Scoring (#251) */
|
|
2021
2022
|
React.createElement(
|
|
2022
2023
|
"div",
|
|
@@ -2054,6 +2055,74 @@ window.__ModuleLoader__.load({
|
|
|
2054
2055
|
"(" + (t("aggregator.workbench_companion_note") || "safe shadow Git checkpoints before file promotion") + ")"
|
|
2055
2056
|
)
|
|
2056
2057
|
),
|
|
2058
|
+
/* Quota-Aware Provider Balancing (#253) */
|
|
2059
|
+
React.createElement(
|
|
2060
|
+
"div",
|
|
2061
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
|
|
2062
|
+
React.createElement(
|
|
2063
|
+
"label",
|
|
2064
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2065
|
+
React.createElement("input", {
|
|
2066
|
+
type: "checkbox",
|
|
2067
|
+
checked: Boolean(currentPreset.quota_aware_balancing_enabled),
|
|
2068
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, quota_aware_balancing_enabled: e.target.checked })),
|
|
2069
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2070
|
+
}),
|
|
2071
|
+
t("aggregator.quota_balancing_title") || "\u2696\uFE0F Quota-Aware Provider Balancing"
|
|
2072
|
+
),
|
|
2073
|
+
React.createElement(
|
|
2074
|
+
"div",
|
|
2075
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2076
|
+
t("aggregator.quota_balancing_desc") || "Automatically substitute low-quota candidate models in Layer 1 to preserve rate limits for the final aggregator."
|
|
2077
|
+
),
|
|
2078
|
+
React.createElement(
|
|
2079
|
+
"div",
|
|
2080
|
+
{ style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", marginLeft: 22, marginTop: 2, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" } },
|
|
2081
|
+
"\u{1F517} Companion Plugins:",
|
|
2082
|
+
React.createElement(
|
|
2083
|
+
"a",
|
|
2084
|
+
{ href: "https://github.com/GooDAnDReaDY/dsh-key-limits", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
|
|
2085
|
+
"@goodandready/dsh-key-limits"
|
|
2086
|
+
),
|
|
2087
|
+
"/",
|
|
2088
|
+
React.createElement(
|
|
2089
|
+
"a",
|
|
2090
|
+
{ href: "https://github.com/GooDAnDReaDY/dsh-subscriptions", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
|
|
2091
|
+
"@goodandready/dsh-subscriptions"
|
|
2092
|
+
)
|
|
2093
|
+
)
|
|
2094
|
+
),
|
|
2095
|
+
/* Self-Healing Post-Promotion Build Loop (#252) */
|
|
2096
|
+
React.createElement(
|
|
2097
|
+
"div",
|
|
2098
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
|
|
2099
|
+
React.createElement(
|
|
2100
|
+
"label",
|
|
2101
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2102
|
+
React.createElement("input", {
|
|
2103
|
+
type: "checkbox",
|
|
2104
|
+
checked: Boolean(currentPreset.self_healing_enabled),
|
|
2105
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, self_healing_enabled: e.target.checked })),
|
|
2106
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2107
|
+
}),
|
|
2108
|
+
t("aggregator.self_healing_title") || "\u{1FA7A} Self-Healing Post-Promotion Build Loop"
|
|
2109
|
+
),
|
|
2110
|
+
React.createElement(
|
|
2111
|
+
"div",
|
|
2112
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2113
|
+
t("aggregator.self_healing_desc") || "Verify promoted files with a test command, request hotfix patches from the aggregator on failure, or auto-rollback to the Time Machine checkpoint."
|
|
2114
|
+
),
|
|
2115
|
+
React.createElement(
|
|
2116
|
+
"div",
|
|
2117
|
+
{ style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", marginLeft: 22, marginTop: 2, display: "flex", alignItems: "center", gap: 6 } },
|
|
2118
|
+
"\u{1F517} Companion Plugin:",
|
|
2119
|
+
React.createElement(
|
|
2120
|
+
"a",
|
|
2121
|
+
{ href: "https://github.com/GooDAnDReaDY/dsh-time-machine", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
|
|
2122
|
+
"@goodandready/dsh-time-machine"
|
|
2123
|
+
)
|
|
2124
|
+
)
|
|
2125
|
+
),
|
|
2057
2126
|
/* Graceful Degradation & Local Fallback (#175, #176) */
|
|
2058
2127
|
React.createElement(
|
|
2059
2128
|
"div",
|
package/lib/index.js
CHANGED
|
@@ -401,3 +401,15 @@ export {
|
|
|
401
401
|
analyzeCandidateUncertainty,
|
|
402
402
|
formatUncertaintyAnnotations,
|
|
403
403
|
} from './moa-uncertainty.js'
|
|
404
|
+
export {
|
|
405
|
+
classifyPromptDomain,
|
|
406
|
+
assignAdaptivePersonas,
|
|
407
|
+
DOMAIN_PERSONA_POOLS,
|
|
408
|
+
} from './moa-adaptive-personas.js'
|
|
409
|
+
export {
|
|
410
|
+
isGitRepository,
|
|
411
|
+
createSwarmWorktree,
|
|
412
|
+
collectSwarmWorktreeDiff,
|
|
413
|
+
cleanupSwarmWorktrees,
|
|
414
|
+
runSubagentSwarm,
|
|
415
|
+
} from './moa-swarm.js'
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-Adaptive Dynamic Persona Synthesis for Reference Models (#254)
|
|
3
|
+
* Classifies prompt technical domain and synthesizes complementary engineering personas
|
|
4
|
+
* dynamically so each candidate approaches the task from a distinct expert angle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DOMAIN_PERSONA_POOLS = {
|
|
8
|
+
database: [
|
|
9
|
+
{
|
|
10
|
+
id: 'dba_index_specialist',
|
|
11
|
+
title: 'DBA Index Specialist',
|
|
12
|
+
prompt: 'You are a Principal DBA Index Specialist. Focus on query execution plans, composite indexing, lock contention prevention, and optimal read/write throughput.',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'zero_downtime_migration_guard',
|
|
16
|
+
title: 'Zero-Downtime Migration Guard',
|
|
17
|
+
prompt: 'You are a Zero-Downtime Migration Guard. Ensure backward-compatible schema evolutions, safe expand-and-contract patterns, and rollback safety.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'orm_query_architect',
|
|
21
|
+
title: 'ORM & Query Architect',
|
|
22
|
+
prompt: 'You are an ORM & Query Architect. Focus on eliminating N+1 queries, clean repository abstractions, transaction isolation levels, and strict data integrity.',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
frontend: [
|
|
26
|
+
{
|
|
27
|
+
id: 'a11y_ux_specialist',
|
|
28
|
+
title: 'Accessibility & UX Specialist',
|
|
29
|
+
prompt: 'You are an Accessibility & UX Specialist. Ensure WCAG 2.1 AA compliance, keyboard navigation, ARIA semantics, and responsive user feedback.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'css_design_systems_architect',
|
|
33
|
+
title: 'CSS & Design Systems Architect',
|
|
34
|
+
prompt: 'You are a CSS & Design Systems Architect. Focus on design tokens, layout resilience, fluid typography, zero layout shift, and clean component composition.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'state_rendering_optimizer',
|
|
38
|
+
title: 'State & Rendering Optimizer',
|
|
39
|
+
prompt: 'You are a Frontend State & Rendering Optimizer. Focus on minimizing unnecessary re-renders, memoization, deterministic state transitions, and bundle efficiency.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
security: [
|
|
43
|
+
{
|
|
44
|
+
id: 'offensive_security_auditor',
|
|
45
|
+
title: 'Offensive Security Auditor',
|
|
46
|
+
prompt: 'You are an Offensive Security Auditor. Proactively hunt for injection vectors, SSRF, path traversal, auth bypass, and privilege escalation.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'defensive_hardening_specialist',
|
|
50
|
+
title: 'Defensive Hardening Specialist',
|
|
51
|
+
prompt: 'You are a Defensive Hardening Specialist. Enforce least-privilege access, strict input validation, fail-closed guards, and cryptographic best practices.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'runtime_sandbox_guardian',
|
|
55
|
+
title: 'Runtime & Sandbox Guardian',
|
|
56
|
+
prompt: 'You are a Runtime & Sandbox Guardian. Focus on resource limits, rate limiting, secret sanitization, and zero-trust boundary enforcement.',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
performance: [
|
|
60
|
+
{
|
|
61
|
+
id: 'algorithmic_optimizer',
|
|
62
|
+
title: 'Algorithmic Complexity Optimizer',
|
|
63
|
+
prompt: 'You are an Algorithmic Complexity Optimizer. Replace O(N^2) bottlenecks with optimal data structures, streaming pipelines, and bounded memory usage.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'concurrency_profiler',
|
|
67
|
+
title: 'Memory & Concurrency Profiler',
|
|
68
|
+
prompt: 'You are a Memory & Concurrency Profiler. Prevent event-loop blocking, race conditions, memory leaks, and unbounded promise queues.',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
testing: [
|
|
72
|
+
{
|
|
73
|
+
id: 'edge_case_fuzzer',
|
|
74
|
+
title: 'Edge-Case & Fuzzing Specialist',
|
|
75
|
+
prompt: 'You are an Edge-Case & Fuzzing Specialist. Target boundary conditions, malformed inputs, concurrency races, and failure recovery paths.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'contract_test_architect',
|
|
79
|
+
title: 'Integration & Contract Test Architect',
|
|
80
|
+
prompt: 'You are an Integration & Contract Test Architect. Build deterministic, isolated, fast-running test suites with zero flakiness.',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
architecture: [
|
|
84
|
+
{
|
|
85
|
+
id: 'ddd_architect',
|
|
86
|
+
title: 'Domain-Driven Design Architect',
|
|
87
|
+
prompt: 'You are a Domain-Driven Design Architect. Enforce clean module boundaries, single responsibility, explicit contracts, and low coupling.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'resilience_engineer',
|
|
91
|
+
title: 'Resilience & Fault-Tolerance Engineer',
|
|
92
|
+
prompt: 'You are a Resilience & Fault-Tolerance Engineer. Design idempotent operations, graceful degradation, circuit breakers, and clear error taxonomy.',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const DOMAIN_RULES = [
|
|
98
|
+
{ domain: 'database', pattern: /\b(sql|postgres|mysql|sqlite|prisma|drizzle|typeorm|migration|index|schema|table|query|database|db|transaction)\b/i },
|
|
99
|
+
{ domain: 'security', pattern: /\b(security|auth|jwt|oauth|csrf|xss|ssrf|injection|traversal|permission|encrypt|hash|token|secret|vulnerability|cve)\b/i },
|
|
100
|
+
{ domain: 'frontend', pattern: /\b(react|vue|svelte|css|tailwind|html|dom|component|ui|ux|button|modal|layout|responsive|aria|accessibility|style)\b/i },
|
|
101
|
+
{ domain: 'performance', pattern: /\b(performance|optimize|latency|throughput|cache|memory|leak|benchmark|bottleneck|fast|profil|concurrent|parallel)\b/i },
|
|
102
|
+
{ domain: 'testing', pattern: /\b(test|unit|integration|e2e|mock|assert|coverage|vitest|jest|playwright|tdd|regression)\b/i },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Classifies a user prompt into a specialized technical domain.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} prompt
|
|
109
|
+
* @returns {{ domain: string, confidence: number, matchedKeywords: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
export function classifyPromptDomain(prompt) {
|
|
112
|
+
const text = String(prompt || '').trim()
|
|
113
|
+
if (!text) return { domain: 'architecture', confidence: 0.5, matchedKeywords: [] }
|
|
114
|
+
|
|
115
|
+
let bestDomain = 'architecture'
|
|
116
|
+
let bestCount = 0
|
|
117
|
+
let bestMatches = []
|
|
118
|
+
|
|
119
|
+
for (const rule of DOMAIN_RULES) {
|
|
120
|
+
const matches = [...text.matchAll(new RegExp(rule.pattern.source, 'gi'))].map((m) => m[0].toLowerCase())
|
|
121
|
+
const unique = [...new Set(matches)]
|
|
122
|
+
if (unique.length > bestCount) {
|
|
123
|
+
bestCount = unique.length
|
|
124
|
+
bestDomain = rule.domain
|
|
125
|
+
bestMatches = unique
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
domain: bestDomain,
|
|
131
|
+
confidence: bestCount > 0 ? Math.min(0.98, Number((0.6 + bestCount * 0.12).toFixed(2))) : 0.55,
|
|
132
|
+
matchedKeywords: bestMatches,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Synthesizes and assigns complementary domain-specific personas to reference model slots.
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<object>} references
|
|
140
|
+
* @param {string} prompt
|
|
141
|
+
* @returns {{ domain: string, references: Array<object>, personasAssigned: Array<object> }}
|
|
142
|
+
*/
|
|
143
|
+
export function assignAdaptivePersonas(references, prompt) {
|
|
144
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
145
|
+
return { domain: 'architecture', references: [], personasAssigned: [] }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { domain } = classifyPromptDomain(prompt)
|
|
149
|
+
const pool = DOMAIN_PERSONA_POOLS[domain] || DOMAIN_PERSONA_POOLS.architecture
|
|
150
|
+
const fallbackPool = DOMAIN_PERSONA_POOLS.architecture
|
|
151
|
+
|
|
152
|
+
const personasAssigned = []
|
|
153
|
+
const enrichedRefs = references.map((slot, idx) => {
|
|
154
|
+
const chosen = pool[idx % pool.length] || fallbackPool[idx % fallbackPool.length]
|
|
155
|
+
personasAssigned.push({
|
|
156
|
+
candidateIndex: idx + 1,
|
|
157
|
+
model: `${slot.provider || ''}:${slot.model || ''}`,
|
|
158
|
+
domain,
|
|
159
|
+
personaId: chosen.id,
|
|
160
|
+
personaTitle: chosen.title,
|
|
161
|
+
})
|
|
162
|
+
return {
|
|
163
|
+
...slot,
|
|
164
|
+
adaptive_domain: domain,
|
|
165
|
+
adaptive_persona_id: chosen.id,
|
|
166
|
+
adaptive_persona_title: chosen.title,
|
|
167
|
+
adaptive_persona_prompt: `[ADAPTIVE EXPERT ROLE — ${chosen.title} (${domain.toUpperCase()})]: ${chosen.prompt}`,
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
domain,
|
|
173
|
+
references: enrichedRefs,
|
|
174
|
+
personasAssigned,
|
|
175
|
+
}
|
|
176
|
+
}
|
package/lib/moa-candidates.js
CHANGED
|
@@ -85,9 +85,9 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
85
85
|
references.forEach((slot, i) => {
|
|
86
86
|
const label = slotLabel(slot)
|
|
87
87
|
const role = slot?.role_persona || slot?.role || 'general'
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const basePersona = (role && ROLE_PERSONA_PROMPTS[role]) ? `\n\n${ROLE_PERSONA_PROMPTS[role]}` : ''
|
|
89
|
+
const adaptiveExtra = slot?.adaptive_persona_prompt ? `\n\n${slot.adaptive_persona_prompt}` : ''
|
|
90
|
+
const persona = basePersona + adaptiveExtra
|
|
91
91
|
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
92
92
|
|
|
93
93
|
// Feature 7: Per-candidate temperature or temperature gradient
|
|
@@ -140,7 +140,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
140
140
|
index: i + 1,
|
|
141
141
|
slot,
|
|
142
142
|
label,
|
|
143
|
-
role_persona: role,
|
|
143
|
+
role_persona: slot?.adaptive_persona_title || role,
|
|
144
|
+
...(slot?.adaptive_domain ? { adaptive_domain: slot.adaptive_domain } : {}),
|
|
144
145
|
temperature: effectiveTemperature,
|
|
145
146
|
text,
|
|
146
147
|
usage: costInfo,
|
package/lib/moa-parser.js
CHANGED
|
@@ -56,12 +56,18 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
let remainder = text.slice(4).trim()
|
|
60
|
+
let isSwarm = false
|
|
61
|
+
if (/^--swarm(?:\s+|$)/i.test(remainder)) {
|
|
62
|
+
isSwarm = true
|
|
63
|
+
remainder = remainder.replace(/^--swarm\s*/i, '').trim()
|
|
64
|
+
}
|
|
60
65
|
if (!remainder) {
|
|
61
66
|
return {
|
|
62
67
|
presetName: defaultPreset,
|
|
63
68
|
prompt: '',
|
|
64
69
|
isExplicit: false,
|
|
70
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -71,6 +77,7 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
71
77
|
presetName: matchPreset[1],
|
|
72
78
|
prompt: matchPreset[2] || '',
|
|
73
79
|
isExplicit: true,
|
|
80
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -80,6 +87,7 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
80
87
|
presetName: matchShortPreset[1],
|
|
81
88
|
prompt: matchShortPreset[2] || '',
|
|
82
89
|
isExplicit: true,
|
|
90
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
|
|
@@ -100,6 +108,7 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
100
108
|
presetName: defaultPreset,
|
|
101
109
|
prompt: remainder,
|
|
102
110
|
isExplicit: false,
|
|
111
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota-Aware Provider Balancing (#253)
|
|
3
|
+
* Queries provider/model rate limits and quota health via dsh-key-limits and dsh-subscriptions.
|
|
4
|
+
* Dynamically substitutes exhausted or low-quota reference slots with healthy fallback models
|
|
5
|
+
* to protect the final aggregator quota and prevent 429 pipeline aborts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { service } from './services.js'
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { slotLabel } from './moa-prompts.js'
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FALLBACK_POOL = [
|
|
13
|
+
{ provider: 'deepseek', model: 'deepseek-reasoner' },
|
|
14
|
+
{ provider: 'deepseek', model: 'deepseek-chat' },
|
|
15
|
+
{ provider: 'qwen', model: 'qwen-2.5-coder-32b-instruct' },
|
|
16
|
+
{ provider: 'openrouter', model: 'deepseek/deepseek-r1' },
|
|
17
|
+
{ provider: 'ollama', model: 'qwen2.5-coder:32b' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inspects quota status for a given { provider, model } slot via dsh-key-limits or dsh-subscriptions.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} ctx - Cordis context
|
|
24
|
+
* @param {{ provider: string, model: string }} slot
|
|
25
|
+
* @param {object} [options]
|
|
26
|
+
* @returns {Promise<{ exhausted: boolean, remainingRatio: number, reason: string | null }>}
|
|
27
|
+
*/
|
|
28
|
+
export async function checkSlotQuotaStatus(ctx, slot, options = {}) {
|
|
29
|
+
const provider = String(slot?.provider || '').trim()
|
|
30
|
+
const model = String(slot?.model || '').trim()
|
|
31
|
+
if (!provider) return { exhausted: false, remainingRatio: 1, reason: null }
|
|
32
|
+
|
|
33
|
+
const minRemainingRatio = Number.isFinite(options.minRemainingRatio) ? options.minRemainingRatio : 0.1
|
|
34
|
+
|
|
35
|
+
const keyLimits = options.keyLimitsService || service(ctx, 'keyLimits')
|
|
36
|
+
if (keyLimits) {
|
|
37
|
+
try {
|
|
38
|
+
let status = null
|
|
39
|
+
if (typeof keyLimits.getQuotaStatus === 'function') {
|
|
40
|
+
status = await keyLimits.getQuotaStatus(provider, model)
|
|
41
|
+
} else if (typeof keyLimits.checkLimit === 'function') {
|
|
42
|
+
status = await keyLimits.checkLimit(provider, model)
|
|
43
|
+
} else if (typeof keyLimits.getProviderStatus === 'function') {
|
|
44
|
+
status = await keyLimits.getProviderStatus(provider)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (status) {
|
|
48
|
+
if (status.exhausted === true || status.rateLimited === true || status.blocked === true) {
|
|
49
|
+
return {
|
|
50
|
+
exhausted: true,
|
|
51
|
+
remainingRatio: 0,
|
|
52
|
+
reason: status.reason || 'Quota exhausted or rate-limited in dsh-key-limits',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const rem = Number(status.remaining ?? status.remainingRequests ?? status.remainingTokens)
|
|
56
|
+
const lim = Number(status.limit ?? status.maxRequests ?? status.maxTokens)
|
|
57
|
+
if (Number.isFinite(rem) && Number.isFinite(lim) && lim > 0) {
|
|
58
|
+
const ratio = rem / lim
|
|
59
|
+
if (ratio < minRemainingRatio) {
|
|
60
|
+
return {
|
|
61
|
+
exhausted: true,
|
|
62
|
+
remainingRatio: ratio,
|
|
63
|
+
reason: `Low quota remaining (${Math.round(ratio * 100)}%) in dsh-key-limits`,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { exhausted: false, remainingRatio: ratio, reason: null }
|
|
67
|
+
}
|
|
68
|
+
if (Number.isFinite(status.remainingRatio)) {
|
|
69
|
+
if (status.remainingRatio < minRemainingRatio) {
|
|
70
|
+
return {
|
|
71
|
+
exhausted: true,
|
|
72
|
+
remainingRatio: status.remainingRatio,
|
|
73
|
+
reason: `Low quota ratio (${Math.round(status.remainingRatio * 100)}%) in dsh-key-limits`,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { exhausted: false, remainingRatio: status.remainingRatio, reason: null }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.warn(`[dsh-moa:quota] Error querying dsh-key-limits for ${provider}:${model}:`, err?.message || err)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const subscriptions = options.subscriptionsService || service(ctx, 'subscriptions')
|
|
85
|
+
if (subscriptions) {
|
|
86
|
+
try {
|
|
87
|
+
let subStatus = null
|
|
88
|
+
if (typeof subscriptions.getSubscriptionStatus === 'function') {
|
|
89
|
+
subStatus = await subscriptions.getSubscriptionStatus(provider, model)
|
|
90
|
+
} else if (typeof subscriptions.checkQuota === 'function') {
|
|
91
|
+
subStatus = await subscriptions.checkQuota(provider, model)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (subStatus) {
|
|
95
|
+
if (subStatus.exhausted === true || subStatus.active === false || subStatus.expired === true) {
|
|
96
|
+
return {
|
|
97
|
+
exhausted: true,
|
|
98
|
+
remainingRatio: 0,
|
|
99
|
+
reason: subStatus.reason || 'Subscription quota exhausted or inactive in dsh-subscriptions',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (Number.isFinite(subStatus.remainingRatio) && subStatus.remainingRatio < minRemainingRatio) {
|
|
103
|
+
return {
|
|
104
|
+
exhausted: true,
|
|
105
|
+
remainingRatio: subStatus.remainingRatio,
|
|
106
|
+
reason: `Low subscription quota (${Math.round(subStatus.remainingRatio * 100)}%) in dsh-subscriptions`,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logger.warn(`[dsh-moa:quota] Error querying dsh-subscriptions for ${provider}:${model}:`, err?.message || err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { exhausted: false, remainingRatio: 1, reason: null }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Balances candidate reference slots before Layer 1 execution.
|
|
120
|
+
* Preserves quota for the aggregator by substituting low-quota/exhausted slots
|
|
121
|
+
* with healthy fallback models.
|
|
122
|
+
*
|
|
123
|
+
* @param {Array<{ provider: string, model: string }>} references
|
|
124
|
+
* @param {object} options
|
|
125
|
+
* @returns {Promise<{ balancedReferences: Array<{ provider: string, model: string }>, substitutions: Array<object> }>}
|
|
126
|
+
*/
|
|
127
|
+
export async function balanceCandidateQuotas(references = [], options = {}) {
|
|
128
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
129
|
+
return { balancedReferences: [], substitutions: [] }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ctx = options.ctx || null
|
|
133
|
+
const aggregator = options.aggregator || null
|
|
134
|
+
const customFallbacks = Array.isArray(options.fallbackPool) && options.fallbackPool.length > 0
|
|
135
|
+
? options.fallbackPool
|
|
136
|
+
: DEFAULT_FALLBACK_POOL
|
|
137
|
+
|
|
138
|
+
const usedKeys = new Set(references.map((r) => slotLabel(r)))
|
|
139
|
+
const balancedReferences = []
|
|
140
|
+
const substitutions = []
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < references.length; i++) {
|
|
143
|
+
const slot = references[i]
|
|
144
|
+
const label = slotLabel(slot)
|
|
145
|
+
|
|
146
|
+
// Reserve higher threshold (25%) if this candidate uses the exact same provider:model as the final aggregator
|
|
147
|
+
const isSameAsAggregator = aggregator && slotLabel(aggregator) === label
|
|
148
|
+
const minRemainingRatio = isSameAsAggregator ? 0.25 : (options.minRemainingRatio ?? 0.1)
|
|
149
|
+
|
|
150
|
+
const status = await checkSlotQuotaStatus(ctx, slot, {
|
|
151
|
+
...options,
|
|
152
|
+
minRemainingRatio,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (!status.exhausted) {
|
|
156
|
+
balancedReferences.push(slot)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find a healthy replacement from customFallbacks
|
|
161
|
+
let replacement = null
|
|
162
|
+
for (const candidateFallback of customFallbacks) {
|
|
163
|
+
const fbLabel = slotLabel(candidateFallback)
|
|
164
|
+
if (fbLabel === label) continue
|
|
165
|
+
const fbStatus = await checkSlotQuotaStatus(ctx, candidateFallback, options)
|
|
166
|
+
if (!fbStatus.exhausted) {
|
|
167
|
+
replacement = { ...slot, provider: candidateFallback.provider, model: candidateFallback.model, replacedFrom: label }
|
|
168
|
+
usedKeys.add(fbLabel)
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (replacement) {
|
|
174
|
+
balancedReferences.push(replacement)
|
|
175
|
+
substitutions.push({
|
|
176
|
+
index: i + 1,
|
|
177
|
+
original: slot,
|
|
178
|
+
replacement: { provider: replacement.provider, model: replacement.model },
|
|
179
|
+
reason: status.reason,
|
|
180
|
+
})
|
|
181
|
+
logger.info(`[dsh-moa:quota] Replaced slot #${i + 1} (${label}) -> ${slotLabel(replacement)} (${status.reason})`)
|
|
182
|
+
} else {
|
|
183
|
+
balancedReferences.push(slot)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { balancedReferences, substitutions }
|
|
188
|
+
}
|
package/lib/moa-schema.js
CHANGED
|
@@ -64,6 +64,12 @@ export const PresetSchema = z.object({
|
|
|
64
64
|
local_fallback_models: z.array(ModelSlotSchema).default([]),
|
|
65
65
|
interactive_workbench_enabled: z.boolean().default(false),
|
|
66
66
|
uncertainty_scoring_enabled: z.boolean().default(false),
|
|
67
|
+
subagent_swarm_enabled: z.boolean().default(false),
|
|
68
|
+
adaptive_personas_enabled: z.boolean().default(false),
|
|
69
|
+
quota_aware_balancing_enabled: z.boolean().default(false),
|
|
70
|
+
self_healing_enabled: z.boolean().default(false),
|
|
71
|
+
self_healing_max_retries: z.number().default(2),
|
|
72
|
+
self_healing_command: z.string().default("npm test"),
|
|
67
73
|
})
|
|
68
74
|
|
|
69
75
|
export const Config = z.object({
|
|
@@ -74,6 +80,12 @@ export const Config = z.object({
|
|
|
74
80
|
ask_clarifying_questions: z.boolean().default(true).volatile(),
|
|
75
81
|
test_gate_enabled: z.boolean().default(false).volatile(),
|
|
76
82
|
uncertainty_scoring_enabled: z.boolean().default(false).volatile(),
|
|
83
|
+
subagent_swarm_enabled: z.boolean().default(false).volatile(),
|
|
84
|
+
adaptive_personas_enabled: z.boolean().default(false).volatile(),
|
|
85
|
+
quota_aware_balancing_enabled: z.boolean().default(false).volatile(),
|
|
86
|
+
self_healing_enabled: z.boolean().default(false).volatile(),
|
|
87
|
+
self_healing_max_retries: z.number().default(2).volatile(),
|
|
88
|
+
self_healing_command: z.string().default("npm test").volatile(),
|
|
77
89
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
78
90
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
79
91
|
presets: z.array(PresetSchema).default([
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Healing Post-Promotion Build Loop with dsh-time-machine Auto-Rollback (#252)
|
|
3
|
+
* Executes verification command after file promotion, dispatches compiler/test error logs
|
|
4
|
+
* to the MoA aggregator for hotfix generation up to maxRetries times, and automatically
|
|
5
|
+
* rolls back to the pre-promotion dsh-time-machine snapshot if verification still fails.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from 'node:child_process'
|
|
9
|
+
import { promisify } from 'node:util'
|
|
10
|
+
import fs from 'node:fs/promises'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { logger } from './logger.js'
|
|
13
|
+
import { bestEffort } from './best-effort.js'
|
|
14
|
+
import { extractFileBlocks } from './workspace/fs.js'
|
|
15
|
+
import { isSafeRelativePath, assertPathContained, isLoopbackUrl } from './workspace/security.js'
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs a verification command in the target workspace.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} cwd
|
|
23
|
+
* @param {string} commandStr
|
|
24
|
+
* @param {object} [options]
|
|
25
|
+
* @returns {Promise<{ passed: boolean, exitCode: number, output: string }>}
|
|
26
|
+
*/
|
|
27
|
+
export async function runVerificationCommand(cwd, commandStr = 'npm test', options = {}) {
|
|
28
|
+
if (typeof options.execFn === 'function') {
|
|
29
|
+
return await options.execFn(cwd, commandStr)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = String(commandStr || 'npm test').trim()
|
|
33
|
+
const isWin = process.platform === 'win32'
|
|
34
|
+
const shell = isWin ? 'cmd.exe' : '/bin/sh'
|
|
35
|
+
const shellArgs = isWin ? ['/d', '/s', '/c', trimmed] : ['-c', trimmed]
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { stdout, stderr } = await execFileAsync(shell, shellArgs, {
|
|
39
|
+
cwd,
|
|
40
|
+
timeout: (options.timeoutSec ?? 60) * 1000,
|
|
41
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
42
|
+
signal: options.signal,
|
|
43
|
+
})
|
|
44
|
+
return {
|
|
45
|
+
passed: true,
|
|
46
|
+
exitCode: 0,
|
|
47
|
+
output: `${stdout || ''}\n${stderr || ''}`.trim(),
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const out = `${err?.stdout || ''}\n${err?.stderr || ''}\n${err?.message || ''}`.trim()
|
|
51
|
+
return {
|
|
52
|
+
passed: false,
|
|
53
|
+
exitCode: typeof err?.code === 'number' ? err.code : 1,
|
|
54
|
+
output: out.slice(0, 12000),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Triggers an automatic rollback to a pre-promotion dsh-time-machine snapshot.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} baseDir
|
|
63
|
+
* @param {object} snapshot
|
|
64
|
+
* @param {object} [options]
|
|
65
|
+
* @returns {Promise<{ rolledBack: boolean, snapshotId: string | null }>}
|
|
66
|
+
*/
|
|
67
|
+
export async function rollbackToTimeMachineSnapshot(baseDir, snapshot, options = {}) {
|
|
68
|
+
const snapshotId = typeof snapshot === 'string'
|
|
69
|
+
? snapshot
|
|
70
|
+
: (snapshot?.id || snapshot?.snapshotId || snapshot?.commit || snapshot?.hash || null)
|
|
71
|
+
|
|
72
|
+
if (typeof options.rollbackFn === 'function') {
|
|
73
|
+
const res = await bestEffort('custom rollbackFn', () => options.rollbackFn(baseDir, snapshotId, snapshot))
|
|
74
|
+
return { rolledBack: Boolean(res), snapshotId }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (options.timeMachineEngine) {
|
|
78
|
+
const fn = options.timeMachineEngine.restoreSnapshot || options.timeMachineEngine.rollback
|
|
79
|
+
if (typeof fn === 'function') {
|
|
80
|
+
const res = await bestEffort('dsh-time-machine direct restoreSnapshot', () =>
|
|
81
|
+
fn.call(options.timeMachineEngine, snapshotId, { cwd: baseDir })
|
|
82
|
+
)
|
|
83
|
+
if (res) return { rolledBack: true, snapshotId }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const port = options.port || options.webServerPort || process.env.DSH_PORT || process.env.PORT
|
|
88
|
+
if (!port && !options.timeMachineRollbackUrl) {
|
|
89
|
+
logger.warn('[dsh-moa:self-healing] Cannot rollback: webServerPort is unknown and no timeMachineEngine/rollbackFn provided')
|
|
90
|
+
return { rolledBack: false, snapshotId }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const endpoint = options.timeMachineRollbackUrl || `http://127.0.0.1:${port}/dsh-time-machine/restore`
|
|
94
|
+
if (!isLoopbackUrl(endpoint)) {
|
|
95
|
+
logger.warn(`[dsh-moa:self-healing] Blocked non-loopback rollback URL: ${endpoint}`)
|
|
96
|
+
return { rolledBack: false, snapshotId }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const restored = await bestEffort('dsh-time-machine HTTP rollback', async () => {
|
|
100
|
+
const res = await fetch(endpoint, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({ id: snapshotId, snapshotId, cwd: baseDir }),
|
|
104
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 5000),
|
|
105
|
+
redirect: 'error',
|
|
106
|
+
})
|
|
107
|
+
return res.ok
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return { rolledBack: Boolean(restored), snapshotId }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Applies extracted hotfix files directly to the workspace directory.
|
|
115
|
+
*/
|
|
116
|
+
async function applyHotfixFiles(cwd, files = []) {
|
|
117
|
+
const written = []
|
|
118
|
+
for (const f of files) {
|
|
119
|
+
const rel = f.relativePath || f.path
|
|
120
|
+
if (!rel || !isSafeRelativePath(rel)) continue
|
|
121
|
+
const targetPath = assertPathContained(cwd, rel)
|
|
122
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
|
123
|
+
await fs.writeFile(targetPath, f.content, 'utf8')
|
|
124
|
+
written.push(rel)
|
|
125
|
+
}
|
|
126
|
+
return written
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Runs the post-promotion self-healing build loop.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} params
|
|
133
|
+
* @returns {Promise<{
|
|
134
|
+
* enabled: boolean,
|
|
135
|
+
* passed: boolean,
|
|
136
|
+
* attempts: number,
|
|
137
|
+
* rolledBack: boolean,
|
|
138
|
+
* snapshotId: string | null,
|
|
139
|
+
* hotfixFiles: string[],
|
|
140
|
+
* lastOutput: string
|
|
141
|
+
* }>}
|
|
142
|
+
*/
|
|
143
|
+
export async function runSelfHealingLoop({
|
|
144
|
+
cwd,
|
|
145
|
+
command = 'npm test',
|
|
146
|
+
maxRetries = 2,
|
|
147
|
+
aggregatorSlot,
|
|
148
|
+
callLlm,
|
|
149
|
+
userPrompt = '',
|
|
150
|
+
promotedFiles = [],
|
|
151
|
+
checkpoint = null,
|
|
152
|
+
promoteOpts = {},
|
|
153
|
+
onProgress = null,
|
|
154
|
+
signal = null,
|
|
155
|
+
execFn = null,
|
|
156
|
+
rollbackFn = null,
|
|
157
|
+
}) {
|
|
158
|
+
const retries = Math.max(0, Math.min(5, Number(maxRetries ?? 2)))
|
|
159
|
+
const hotfixFiles = new Set()
|
|
160
|
+
|
|
161
|
+
if (typeof onProgress === 'function') {
|
|
162
|
+
onProgress(`🩺 *Self-Healing Verification: Running \`${command}\`...*\n`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let check = await runVerificationCommand(cwd, command, { execFn, signal })
|
|
166
|
+
if (check.passed) {
|
|
167
|
+
return {
|
|
168
|
+
enabled: true,
|
|
169
|
+
passed: true,
|
|
170
|
+
attempts: 0,
|
|
171
|
+
rolledBack: false,
|
|
172
|
+
snapshotId: null,
|
|
173
|
+
hotfixFiles: [],
|
|
174
|
+
lastOutput: check.output,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let attempts = 0
|
|
179
|
+
while (!check.passed && attempts < retries) {
|
|
180
|
+
if (signal?.aborted) break
|
|
181
|
+
attempts++
|
|
182
|
+
|
|
183
|
+
if (typeof onProgress === 'function') {
|
|
184
|
+
onProgress(`🔧 *Self-Healing Attempt ${attempts}/${retries}: Requesting hotfix patch from aggregator...*\n`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const fileList = Array.isArray(promotedFiles)
|
|
188
|
+
? promotedFiles.map((f) => (typeof f === 'string' ? f : f.relativePath || f.path)).filter(Boolean)
|
|
189
|
+
: []
|
|
190
|
+
|
|
191
|
+
const hotfixPrompt = [
|
|
192
|
+
`The promoted MoA changes failed the verification command \`${command}\` (exit code ${check.exitCode}).`,
|
|
193
|
+
`Original User Task: ${userPrompt}`,
|
|
194
|
+
`Promoted Files: ${fileList.join(', ') || 'workspace files'}`,
|
|
195
|
+
`\n### Compiler / Test Failure Output:\n\`\`\`\n${check.output}\n\`\`\``,
|
|
196
|
+
`\nProvide a minimal hotfix patch using markdown code blocks with \`path=<relative/path>\` header so the build and tests pass.`,
|
|
197
|
+
].join('\n')
|
|
198
|
+
|
|
199
|
+
const llmRes = await callLlm({
|
|
200
|
+
provider: aggregatorSlot?.provider || 'default',
|
|
201
|
+
model: aggregatorSlot?.model || 'default',
|
|
202
|
+
messages: [
|
|
203
|
+
{
|
|
204
|
+
role: 'system',
|
|
205
|
+
content: 'You are an expert automated build healer. Fix compilation or unit test errors by outputting complete corrected files with ```lang path=relative/path.ext fences.',
|
|
206
|
+
},
|
|
207
|
+
{ role: 'user', content: hotfixPrompt },
|
|
208
|
+
],
|
|
209
|
+
temperature: 0.2,
|
|
210
|
+
maxTokens: 4096,
|
|
211
|
+
signal,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const patchText = typeof llmRes === 'string' ? llmRes : (llmRes?.content || llmRes?.text || '')
|
|
215
|
+
const extracted = extractFileBlocks(patchText)
|
|
216
|
+
if (extracted.length > 0) {
|
|
217
|
+
const applied = await applyHotfixFiles(cwd, extracted)
|
|
218
|
+
for (const f of applied) hotfixFiles.add(f)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
check = await runVerificationCommand(cwd, command, { execFn, signal })
|
|
222
|
+
if (check.passed) {
|
|
223
|
+
if (typeof onProgress === 'function') {
|
|
224
|
+
onProgress(`✅ *Self-Healing Succeeded on attempt ${attempts}/${retries}!*\n`)
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
enabled: true,
|
|
228
|
+
passed: true,
|
|
229
|
+
attempts,
|
|
230
|
+
rolledBack: false,
|
|
231
|
+
snapshotId: null,
|
|
232
|
+
hotfixFiles: [...hotfixFiles],
|
|
233
|
+
lastOutput: check.output,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Verification still failed after maxRetries -> unconditional Time Machine rollback
|
|
239
|
+
const snapshotObj = checkpoint || promoteOpts?.checkpointSnapshot || (Array.isArray(promotedFiles) && promotedFiles._checkpoint) || null
|
|
240
|
+
if (typeof onProgress === 'function') {
|
|
241
|
+
onProgress(`⏪ *Self-Healing exhausted (${attempts}/${retries}). Rolling back workspace via dsh-time-machine...*\n`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const rb = await rollbackToTimeMachineSnapshot(cwd, snapshotObj, {
|
|
245
|
+
...promoteOpts,
|
|
246
|
+
rollbackFn,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
enabled: true,
|
|
251
|
+
passed: false,
|
|
252
|
+
attempts,
|
|
253
|
+
rolledBack: rb.rolledBack,
|
|
254
|
+
snapshotId: rb.snapshotId,
|
|
255
|
+
hotfixFiles: [...hotfixFiles],
|
|
256
|
+
lastOutput: check.output,
|
|
257
|
+
}
|
|
258
|
+
}
|
package/lib/moa-swarm.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MoA-to-Subagents Swarm with Isolated Git Worktrees (#255)
|
|
3
|
+
* Provisions isolated git worktrees (or workspace sandboxes) per candidate,
|
|
4
|
+
* dispatches autonomous subagent/candidate execution, collects git diffs,
|
|
5
|
+
* and guarantees worktree cleanup in finally blocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { execFile } from 'node:child_process'
|
|
12
|
+
import { promisify } from 'node:util'
|
|
13
|
+
import { logger } from './logger.js'
|
|
14
|
+
import { slotLabel } from './moa-prompts.js'
|
|
15
|
+
import { extractFileBlocks } from './workspace/fs.js'
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a directory is inside a valid git repository.
|
|
21
|
+
*/
|
|
22
|
+
export async function isGitRepository(cwd) {
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 })
|
|
25
|
+
return stdout.trim() === 'true'
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates an isolated git worktree (or directory sandbox if not a git repo) for a swarm candidate.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} cwd
|
|
35
|
+
* @param {string} runId
|
|
36
|
+
* @param {number} candidateIndex
|
|
37
|
+
* @returns {Promise<{ worktreePath: string, branchName: string, isGitWorktree: boolean }>}
|
|
38
|
+
*/
|
|
39
|
+
export async function createSwarmWorktree(cwd, runId, candidateIndex) {
|
|
40
|
+
const safeRunId = String(runId || Date.now()).replace(/[^a-zA-Z0-9_-]/g, '')
|
|
41
|
+
const worktreePath = path.join(cwd, '.moa', `swarm-${safeRunId}`, `candidate-${candidateIndex}`)
|
|
42
|
+
const branchName = `moa-swarm/${safeRunId}/cand-${candidateIndex}`
|
|
43
|
+
|
|
44
|
+
await fs.mkdir(path.dirname(worktreePath), { recursive: true })
|
|
45
|
+
|
|
46
|
+
if (await isGitRepository(cwd)) {
|
|
47
|
+
try {
|
|
48
|
+
await execFileAsync('git', ['worktree', 'add', '--detach', worktreePath, 'HEAD'], { cwd, timeout: 10000 })
|
|
49
|
+
return { worktreePath, branchName, isGitWorktree: true }
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.warn(`[dsh-moa] git worktree add failed for candidate-${candidateIndex}, using isolated directory sandbox: ${err?.message || err}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await fs.mkdir(worktreePath, { recursive: true })
|
|
56
|
+
return { worktreePath, branchName, isGitWorktree: false }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Collects git diff and modified files from a candidate's swarm worktree.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} worktreePath
|
|
63
|
+
* @param {boolean} isGitWorktree
|
|
64
|
+
* @returns {Promise<{ diffText: string, modifiedFiles: string[] }>}
|
|
65
|
+
*/
|
|
66
|
+
export async function collectSwarmWorktreeDiff(worktreePath, isGitWorktree = false) {
|
|
67
|
+
if (!existsSync(worktreePath)) {
|
|
68
|
+
return { diffText: '', modifiedFiles: [] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isGitWorktree) {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain'], { cwd: worktreePath, timeout: 5000 })
|
|
74
|
+
const modifiedFiles = statusOut
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((line) => line.slice(3).trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
|
|
79
|
+
const { stdout: diffOut } = await execFileAsync('git', ['diff', 'HEAD'], { cwd: worktreePath, timeout: 5000 })
|
|
80
|
+
return { diffText: diffOut.trim(), modifiedFiles }
|
|
81
|
+
} catch {
|
|
82
|
+
// Fallback to directory scan
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const modifiedFiles = []
|
|
87
|
+
async function scanDir(dir, prefix = '') {
|
|
88
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.name === '.git') continue
|
|
91
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
92
|
+
const full = path.join(dir, entry.name)
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
await scanDir(full, rel)
|
|
95
|
+
} else {
|
|
96
|
+
modifiedFiles.push(rel)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await scanDir(worktreePath)
|
|
101
|
+
return { diffText: '', modifiedFiles }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Guarantees removal of all swarm worktrees and temporary directories.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} cwd
|
|
108
|
+
* @param {string} runId
|
|
109
|
+
* @param {Array<{ worktreePath: string, isGitWorktree: boolean }>} worktrees
|
|
110
|
+
*/
|
|
111
|
+
export async function cleanupSwarmWorktrees(cwd, runId, worktrees = []) {
|
|
112
|
+
for (const wt of worktrees) {
|
|
113
|
+
if (!wt?.worktreePath) continue
|
|
114
|
+
if (wt.isGitWorktree) {
|
|
115
|
+
try {
|
|
116
|
+
await execFileAsync('git', ['worktree', 'remove', '--force', wt.worktreePath], { cwd, timeout: 8000 })
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await fs.rm(wt.worktreePath, { recursive: true, force: true })
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (await isGitRepository(cwd)) {
|
|
125
|
+
try {
|
|
126
|
+
await execFileAsync('git', ['worktree', 'prune'], { cwd, timeout: 5000 })
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const safeRunId = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '')
|
|
131
|
+
if (safeRunId) {
|
|
132
|
+
const swarmRoot = path.join(cwd, '.moa', `swarm-${safeRunId}`)
|
|
133
|
+
try {
|
|
134
|
+
await fs.rm(swarmRoot, { recursive: true, force: true })
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Runs candidates as a swarm with isolated worktrees.
|
|
141
|
+
* Uses ctx.subagent.run if available on Cordis context, falling back to callLlm + worktree materialization.
|
|
142
|
+
*/
|
|
143
|
+
export async function runSubagentSwarm(references, messages, options = {}, callLlm, onProgress) {
|
|
144
|
+
const cwd = options.cwd || process.cwd()
|
|
145
|
+
const runId = options.runId || `run-${Date.now()}`
|
|
146
|
+
const worktrees = []
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (typeof onProgress === 'function') {
|
|
150
|
+
onProgress(`🐝 *Launching MoA Subagent Swarm across ${references.length} isolated worktrees...*\n`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tasks = references.map(async (slot, idx) => {
|
|
154
|
+
const candidateIndex = idx + 1
|
|
155
|
+
const label = slotLabel(slot)
|
|
156
|
+
const wt = await createSwarmWorktree(cwd, runId, candidateIndex)
|
|
157
|
+
worktrees.push(wt)
|
|
158
|
+
|
|
159
|
+
const start = Date.now()
|
|
160
|
+
let responseText = ''
|
|
161
|
+
let usage = { inputTokens: 100, outputTokens: 100, totalTokens: 200, costUsd: 0 }
|
|
162
|
+
|
|
163
|
+
if (options.ctx?.subagent && typeof options.ctx.subagent.run === 'function') {
|
|
164
|
+
const subRes = await options.ctx.subagent.run({
|
|
165
|
+
provider: slot.provider,
|
|
166
|
+
model: slot.model,
|
|
167
|
+
cwd: wt.worktreePath,
|
|
168
|
+
messages,
|
|
169
|
+
signal: options.signal,
|
|
170
|
+
})
|
|
171
|
+
responseText = typeof subRes === 'string' ? subRes : (subRes?.content || subRes?.text || '')
|
|
172
|
+
if (subRes?.usage) usage = subRes.usage
|
|
173
|
+
} else {
|
|
174
|
+
const llmRes = await callLlm({
|
|
175
|
+
provider: slot.provider,
|
|
176
|
+
model: slot.model,
|
|
177
|
+
messages,
|
|
178
|
+
temperature: options.temperature ?? 0.6,
|
|
179
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
180
|
+
signal: options.signal,
|
|
181
|
+
})
|
|
182
|
+
responseText = typeof llmRes === 'string' ? llmRes : (llmRes?.content || llmRes?.text || '')
|
|
183
|
+
if (llmRes?.usage) usage = llmRes.usage
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Materialize any extracted file blocks inside the isolated worktree
|
|
187
|
+
const files = extractFileBlocks(responseText)
|
|
188
|
+
for (const f of files) {
|
|
189
|
+
const targetFile = path.join(wt.worktreePath, f.relativePath || f.path || "")
|
|
190
|
+
await fs.mkdir(path.dirname(targetFile), { recursive: true })
|
|
191
|
+
await fs.writeFile(targetFile, f.content, 'utf8')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const diffInfo = await collectSwarmWorktreeDiff(wt.worktreePath, wt.isGitWorktree)
|
|
195
|
+
const enrichedText = diffInfo.diffText
|
|
196
|
+
? `${responseText}\n\n#### Git Worktree Diff (${wt.branchName}):\n\`\`\`diff\n${diffInfo.diffText}\n\`\`\``
|
|
197
|
+
: responseText
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
index: candidateIndex,
|
|
201
|
+
slot,
|
|
202
|
+
label,
|
|
203
|
+
text: enrichedText,
|
|
204
|
+
files,
|
|
205
|
+
swarmWorktree: wt.worktreePath,
|
|
206
|
+
swarmBranch: wt.branchName,
|
|
207
|
+
modifiedFiles: diffInfo.modifiedFiles,
|
|
208
|
+
usage,
|
|
209
|
+
costUsd: usage.costUsd || 0,
|
|
210
|
+
durationMs: Date.now() - start,
|
|
211
|
+
ok: true,
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return await Promise.all(tasks)
|
|
216
|
+
} finally {
|
|
217
|
+
await cleanupSwarmWorktrees(cwd, runId, worktrees)
|
|
218
|
+
}
|
|
219
|
+
}
|
package/lib/moa-workbench.js
CHANGED
|
@@ -27,6 +27,18 @@ export const COMPANION_PLUGINS = [
|
|
|
27
27
|
url: 'https://github.com/goodandready/dsh-time-machine',
|
|
28
28
|
description: 'Pre-promotion shadow Git checkpoints and rollback points',
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'dsh-key-limits',
|
|
32
|
+
name: 'DSH Key Limits',
|
|
33
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-key-limits',
|
|
34
|
+
description: 'Provider API key rate-limit and quota telemetry',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'dsh-subscriptions',
|
|
38
|
+
name: 'DSH Subscriptions',
|
|
39
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-subscriptions',
|
|
40
|
+
description: 'Subscription tier and quota tracking for LLM providers',
|
|
41
|
+
},
|
|
30
42
|
]
|
|
31
43
|
|
|
32
44
|
/**
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assignAdaptivePersonas } from '../moa-adaptive-personas.js'
|
|
2
|
+
import { runSubagentSwarm } from '../moa-swarm.js'
|
|
1
3
|
import { runReferencesParallel } from '../moa-candidates.js'
|
|
2
4
|
import { extractFileBlocks, writeCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
3
5
|
import { verifyFileSyntax } from '../workspace/diff.js'
|
|
@@ -30,8 +32,15 @@ export async function stageCandidateRound1({
|
|
|
30
32
|
testCommand,
|
|
31
33
|
execFn,
|
|
32
34
|
}) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
+
const lastMsgText = enrichedMessages[enrichedMessages.length - 1]?.content || ''
|
|
36
|
+
const adaptiveRefs = preset?.adaptive_personas_enabled
|
|
37
|
+
? assignAdaptivePersonas(effectiveReferences, typeof lastMsgText === 'string' ? lastMsgText : '').references
|
|
38
|
+
: effectiveReferences
|
|
39
|
+
|
|
40
|
+
const referenceOutputs = preset?.subagent_swarm_enabled
|
|
41
|
+
? await runSubagentSwarm(adaptiveRefs, enrichedMessages, { temperature: refTemp, maxTokens, signal, cwd, runId }, callLlm, onProgress)
|
|
42
|
+
: await runReferencesParallel(
|
|
43
|
+
adaptiveRefs,
|
|
35
44
|
enrichedMessages,
|
|
36
45
|
{
|
|
37
46
|
systemPrompt: candidateSystemPrompt,
|
package/lib/pipeline/context.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { balanceCandidateQuotas } from '../moa-quota-balancer.js'
|
|
1
2
|
import { collectProjectContext, formatProjectContext, isRefinementTask } from '../workspace/scanner.js'
|
|
2
3
|
import { applyBudgetGuardrails, createBudgetAbortPayload } from '../moa-budget.js'
|
|
3
4
|
import { extractPriorTurnBaseline, pruneMultiTurnMessages } from '../moa-context.js'
|
|
@@ -13,6 +14,7 @@ export async function stageProjectContext({
|
|
|
13
14
|
maxTokens,
|
|
14
15
|
startTime,
|
|
15
16
|
onProgress,
|
|
17
|
+
ctx = null,
|
|
16
18
|
}) {
|
|
17
19
|
const rawRefs = (Array.isArray(preset?.reference_models) && preset.reference_models.length > 0)
|
|
18
20
|
? preset.reference_models
|
|
@@ -79,7 +81,23 @@ export async function stageProjectContext({
|
|
|
79
81
|
return { ok: false, abortPayload }
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
let effectiveReferences = budgetCheck.references
|
|
85
|
+
let quotaSubstitutions = []
|
|
86
|
+
if (preset?.quota_aware_balancing_enabled) {
|
|
87
|
+
const fallbackPool = Array.isArray(preset?.local_fallback_models) && preset.local_fallback_models.length > 0
|
|
88
|
+
? preset.local_fallback_models
|
|
89
|
+
: undefined
|
|
90
|
+
const bal = await balanceCandidateQuotas(effectiveReferences, {
|
|
91
|
+
ctx,
|
|
92
|
+
aggregator: primaryJudge,
|
|
93
|
+
fallbackPool,
|
|
94
|
+
})
|
|
95
|
+
effectiveReferences = bal.balancedReferences
|
|
96
|
+
quotaSubstitutions = bal.substitutions
|
|
97
|
+
if (quotaSubstitutions.length > 0 && typeof onProgress === "function") {
|
|
98
|
+
onProgress(`⚖️ *Quota-Aware Balancing: Replaced ${quotaSubstitutions.length} low-quota slot(s) to preserve aggregator budget.*\n`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
83
101
|
if (effectiveReferences.length === 1 && !preset?.curator_synthesis) {
|
|
84
102
|
isFastMode = true
|
|
85
103
|
}
|
|
@@ -103,7 +121,7 @@ export async function stageProjectContext({
|
|
|
103
121
|
isStreamAggregator, isQuorumEnabled, gracePeriodSec, candidateRetries,
|
|
104
122
|
refTimeoutSec, aggTimeoutSec, isBlindEvaluation, isPeerCritiqueEnabled,
|
|
105
123
|
allowCandidateOverride, collectedCtx, isRefinement, projectContext,
|
|
106
|
-
effectiveReferences, priorTurnBaseline, candidateSystemPrompt,
|
|
124
|
+
effectiveReferences, quotaSubstitutions, priorTurnBaseline, candidateSystemPrompt,
|
|
107
125
|
needsQuestions, enrichedMessages,
|
|
108
126
|
}
|
|
109
127
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runSelfHealingLoop } from '../moa-self-healing.js'
|
|
1
2
|
import { logger } from '../logger.js'
|
|
2
3
|
import { parseWinnerIndex, parseRecommendedAssembler } from '../moa-parser.js'
|
|
3
4
|
import { extractFileBlocks, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
@@ -33,6 +34,9 @@ export async function stagePromotion({
|
|
|
33
34
|
historyFilePath,
|
|
34
35
|
collectedCtx,
|
|
35
36
|
ctx = null,
|
|
37
|
+
callLlm = null,
|
|
38
|
+
onProgress = null,
|
|
39
|
+
execFn = null,
|
|
36
40
|
}) {
|
|
37
41
|
const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
|
|
38
42
|
? multiJudgeResult.winningCandidateIndex
|
|
@@ -69,6 +73,24 @@ export async function stagePromotion({
|
|
|
69
73
|
throw promoteErr
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
let selfHealingResult = null
|
|
77
|
+
if (preset?.self_healing_enabled && promotedFiles.length > 0 && typeof callLlm === "function") {
|
|
78
|
+
selfHealingResult = await runSelfHealingLoop({
|
|
79
|
+
cwd,
|
|
80
|
+
command: preset.self_healing_command || "npm test",
|
|
81
|
+
maxRetries: preset.self_healing_max_retries ?? 2,
|
|
82
|
+
aggregatorSlot: chosenJudge,
|
|
83
|
+
callLlm,
|
|
84
|
+
userPrompt,
|
|
85
|
+
promotedFiles,
|
|
86
|
+
checkpoint: promotedFiles.checkpoint || null,
|
|
87
|
+
promoteOpts,
|
|
88
|
+
onProgress,
|
|
89
|
+
signal,
|
|
90
|
+
execFn,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
|
|
73
95
|
const durationMs = Date.now() - startTime
|
|
74
96
|
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
|
|
@@ -125,7 +147,7 @@ export async function stagePromotion({
|
|
|
125
147
|
kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
|
|
126
148
|
references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
|
|
127
149
|
recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
|
|
128
|
-
benchmarkReport, consensus: multiJudgeResult, fileScores, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
150
|
+
benchmarkReport, consensus: multiJudgeResult, fileScores, ...(selfHealingResult ? { selfHealing: selfHealingResult } : {}), ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
129
151
|
usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
|
|
130
152
|
}
|
|
131
153
|
}
|
package/lib/pipeline/runner.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function runMoAPipeline({
|
|
|
40
40
|
|
|
41
41
|
// Stage 1: Context & Configuration
|
|
42
42
|
const ctxStage = await stageProjectContext({
|
|
43
|
-
userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress,
|
|
43
|
+
userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress, ctx,
|
|
44
44
|
})
|
|
45
45
|
if (!ctxStage.ok) return ctxStage.abortPayload
|
|
46
46
|
|
|
@@ -189,7 +189,7 @@ export async function runMoAPipeline({
|
|
|
189
189
|
promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
|
|
190
190
|
userPrompt, preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
|
|
191
191
|
effectiveReferences: ctxStage.effectiveReferences, referenceModels: ctxStage.referenceModels,
|
|
192
|
-
historyFilePath, collectedCtx: ctxStage.collectedCtx, ctx,
|
|
192
|
+
historyFilePath, collectedCtx: ctxStage.collectedCtx, ctx, callLlm, onProgress, execFn,
|
|
193
193
|
})
|
|
194
194
|
if (!promRes.ok) {
|
|
195
195
|
if (promRes.abort) return { error: promRes.error }
|