dual-brain 4.0.1 → 4.1.0
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/hooks/quality-gate.mjs +38 -0
- package/hooks/summary-checkpoint.mjs +182 -0
- package/hooks/vibe-router.mjs +135 -10
- package/package.json +1 -1
package/hooks/quality-gate.mjs
CHANGED
|
@@ -30,6 +30,40 @@ const DUAL_BRAIN = resolve(__dirname, 'dual-brain-review.mjs');
|
|
|
30
30
|
|
|
31
31
|
const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
32
32
|
|
|
33
|
+
const APPROVAL_MAP = {
|
|
34
|
+
low: { recommendation: 'self_check', message: 'Low risk — self-check is sufficient' },
|
|
35
|
+
medium: { recommendation: 'review_recommended', message: 'Medium risk — a code review would catch edge cases' },
|
|
36
|
+
high: { recommendation: 'dual_brain_review', message: 'High risk — recommending dual-brain review for safety' },
|
|
37
|
+
critical: { recommendation: 'user_approval_needed', message: 'Critical risk — this needs your explicit approval before merging' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compute approval recommendation from risk level + profile overrides.
|
|
42
|
+
* Profile escalation: if dual_brain_minimum is at or below the current risk,
|
|
43
|
+
* escalate the recommendation by one tier (e.g. medium → dual_brain_review
|
|
44
|
+
* under quality-first where dual_brain_minimum is 'medium').
|
|
45
|
+
*/
|
|
46
|
+
function computeApproval(risk, profileGate) {
|
|
47
|
+
let effectiveRisk = risk;
|
|
48
|
+
|
|
49
|
+
// Profile escalation: when dual_brain_minimum <= risk and the base
|
|
50
|
+
// recommendation would be below dual_brain_review, escalate one level.
|
|
51
|
+
const riskIdx = RISK_LEVELS.indexOf(risk);
|
|
52
|
+
const dualBrainIdx = RISK_LEVELS.indexOf(profileGate.dual_brain_minimum);
|
|
53
|
+
if (dualBrainIdx >= 0 && riskIdx >= dualBrainIdx && riskIdx < RISK_LEVELS.length - 1) {
|
|
54
|
+
const baseRec = APPROVAL_MAP[risk].recommendation;
|
|
55
|
+
if (baseRec !== 'dual_brain_review' && baseRec !== 'user_approval_needed') {
|
|
56
|
+
effectiveRisk = RISK_LEVELS[riskIdx + 1];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entry = APPROVAL_MAP[effectiveRisk] || APPROVAL_MAP[risk];
|
|
61
|
+
return {
|
|
62
|
+
approval_recommendation: entry.recommendation,
|
|
63
|
+
approval_message: entry.message,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
function loadProfileGateSettings() {
|
|
34
68
|
try {
|
|
35
69
|
return _getProfileOverrides('quality-gate');
|
|
@@ -189,6 +223,7 @@ function main() {
|
|
|
189
223
|
reason: `${sensitivity.risk} risk — below profile floor (${profileGate.sensitivity_floor})`,
|
|
190
224
|
profile_floor: profileGate.sensitivity_floor,
|
|
191
225
|
files: qualifyingFiles,
|
|
226
|
+
...computeApproval(sensitivity.risk, profileGate),
|
|
192
227
|
});
|
|
193
228
|
}
|
|
194
229
|
|
|
@@ -267,6 +302,7 @@ function main() {
|
|
|
267
302
|
}
|
|
268
303
|
|
|
269
304
|
// 9. Build output object — common fields first
|
|
305
|
+
const approval = computeApproval(sensitivity.risk, profileGate);
|
|
270
306
|
const output = {
|
|
271
307
|
gate: gateStatus,
|
|
272
308
|
risk: sensitivity.risk,
|
|
@@ -278,6 +314,8 @@ function main() {
|
|
|
278
314
|
review_path: reviewFile,
|
|
279
315
|
model: reviewResult.model || null,
|
|
280
316
|
auth_type: reviewResult.auth_type || null,
|
|
317
|
+
approval_recommendation: approval.approval_recommendation,
|
|
318
|
+
approval_message: approval.approval_message,
|
|
281
319
|
};
|
|
282
320
|
|
|
283
321
|
// High risk: recommend dual-brain-think in addition
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* getTokenAverages() → moving averages of actual tokens by tier
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { execSync as _execSync } from 'child_process';
|
|
19
20
|
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
20
21
|
import { dirname, join } from 'path';
|
|
21
22
|
import { fileURLToPath } from 'url';
|
|
@@ -65,6 +66,15 @@ function emptySummary() {
|
|
|
65
66
|
dual_brain_useful: false,
|
|
66
67
|
balance_posture: 'no activity yet',
|
|
67
68
|
},
|
|
69
|
+
|
|
70
|
+
// Session handoff fields — enriched checkpoint for cross-session continuity
|
|
71
|
+
session_handoff: {
|
|
72
|
+
gate_passed: [], // completed milestones/tasks this session
|
|
73
|
+
evidence: [], // concrete evidence: commit hashes, file paths, PR URLs
|
|
74
|
+
pickup_prompt: 'none recorded', // one-sentence continuation prompt
|
|
75
|
+
friction: [], // problems encountered during the session
|
|
76
|
+
cross_workstream_patterns: [], // generalizable lessons beyond this task
|
|
77
|
+
},
|
|
68
78
|
};
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -158,6 +168,57 @@ function applyEntry(summary, entry) {
|
|
|
158
168
|
avg.avg_output += (entry.output_tokens - avg.avg_output) / avg.count;
|
|
159
169
|
}
|
|
160
170
|
|
|
171
|
+
// Session handoff: auto-populate from entry metadata
|
|
172
|
+
if (!summary.session_handoff) {
|
|
173
|
+
summary.session_handoff = {
|
|
174
|
+
gate_passed: [], evidence: [], pickup_prompt: 'none recorded',
|
|
175
|
+
friction: [], cross_workstream_patterns: [],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Track completed gates/milestones from quality-gate or review results
|
|
180
|
+
if (entry.type === 'gate_result' && entry.gate === 'pass') {
|
|
181
|
+
summary.session_handoff.gate_passed.push({
|
|
182
|
+
what: entry.reason || 'quality gate passed',
|
|
183
|
+
ts,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Track evidence: file paths from execute-tier entries, commit hashes, PR URLs
|
|
188
|
+
if (tier === 'execute' && entry.files_changed) {
|
|
189
|
+
const files = Array.isArray(entry.files_changed) ? entry.files_changed : [entry.files_changed];
|
|
190
|
+
for (const f of files) {
|
|
191
|
+
if (!summary.session_handoff.evidence.includes(f)) {
|
|
192
|
+
summary.session_handoff.evidence.push(f);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (entry.commit_hash) {
|
|
197
|
+
const ref = `commit:${entry.commit_hash}`;
|
|
198
|
+
if (!summary.session_handoff.evidence.includes(ref)) {
|
|
199
|
+
summary.session_handoff.evidence.push(ref);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (entry.pr_url) {
|
|
203
|
+
if (!summary.session_handoff.evidence.includes(entry.pr_url)) {
|
|
204
|
+
summary.session_handoff.evidence.push(entry.pr_url);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Track friction: failures, escalations, retries
|
|
209
|
+
if (entry.type === 'failure' || entry.escalated || entry.retry) {
|
|
210
|
+
summary.session_handoff.friction.push({
|
|
211
|
+
what: entry.error || entry.reason || 'unknown failure',
|
|
212
|
+
tier,
|
|
213
|
+
provider,
|
|
214
|
+
ts,
|
|
215
|
+
});
|
|
216
|
+
// Keep friction list bounded
|
|
217
|
+
if (summary.session_handoff.friction.length > 50) {
|
|
218
|
+
summary.session_handoff.friction = summary.session_handoff.friction.slice(-50);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
161
222
|
// Codex latencies
|
|
162
223
|
if (entry.codex_startup_ms != null) {
|
|
163
224
|
summary.codex_latencies.push({
|
|
@@ -237,6 +298,125 @@ function getAdaptiveCodexThreshold(date) {
|
|
|
237
298
|
};
|
|
238
299
|
}
|
|
239
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Update a specific session handoff field.
|
|
303
|
+
* Valid keys: gate_passed, evidence, pickup_prompt, friction, cross_workstream_patterns
|
|
304
|
+
*
|
|
305
|
+
* For array fields, `value` is appended (string or object).
|
|
306
|
+
* For pickup_prompt, `value` replaces the current string.
|
|
307
|
+
*/
|
|
308
|
+
function updateHandoff(key, value, date) {
|
|
309
|
+
const arrayFields = ['gate_passed', 'evidence', 'friction', 'cross_workstream_patterns'];
|
|
310
|
+
const validKeys = [...arrayFields, 'pickup_prompt'];
|
|
311
|
+
if (!validKeys.includes(key)) return;
|
|
312
|
+
|
|
313
|
+
const summary = readSummary(date);
|
|
314
|
+
if (!summary.session_handoff) {
|
|
315
|
+
summary.session_handoff = {
|
|
316
|
+
gate_passed: [], evidence: [], pickup_prompt: 'none recorded',
|
|
317
|
+
friction: [], cross_workstream_patterns: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (key === 'pickup_prompt') {
|
|
322
|
+
summary.session_handoff.pickup_prompt = String(value);
|
|
323
|
+
} else if (arrayFields.includes(key)) {
|
|
324
|
+
if (!Array.isArray(summary.session_handoff[key])) {
|
|
325
|
+
summary.session_handoff[key] = [];
|
|
326
|
+
}
|
|
327
|
+
summary.session_handoff[key].push(value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
summary.updated_at = new Date().toISOString();
|
|
331
|
+
atomicWrite(summaryPath(date), summary);
|
|
332
|
+
return summary;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generate a full session checkpoint for handoff.
|
|
337
|
+
*
|
|
338
|
+
* Auto-enriches evidence from git state (changed files, HEAD commit)
|
|
339
|
+
* and builds a pickup prompt if none was set manually.
|
|
340
|
+
*/
|
|
341
|
+
function generateCheckpoint(date) {
|
|
342
|
+
const summary = readSummary(date);
|
|
343
|
+
|
|
344
|
+
if (!summary.session_handoff) {
|
|
345
|
+
summary.session_handoff = {
|
|
346
|
+
gate_passed: [], evidence: [], pickup_prompt: 'none recorded',
|
|
347
|
+
friction: [], cross_workstream_patterns: [],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const handoff = summary.session_handoff;
|
|
352
|
+
|
|
353
|
+
// Auto-enrich evidence from git if available
|
|
354
|
+
try {
|
|
355
|
+
// Current HEAD commit
|
|
356
|
+
const head = _execSync('git rev-parse --short HEAD 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
357
|
+
if (head) {
|
|
358
|
+
const ref = `commit:${head}`;
|
|
359
|
+
if (!handoff.evidence.includes(ref)) {
|
|
360
|
+
handoff.evidence.push(ref);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Changed files in working tree
|
|
365
|
+
const diff = _execSync('git diff --name-only HEAD 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
366
|
+
if (diff) {
|
|
367
|
+
for (const f of diff.split('\n').filter(Boolean)) {
|
|
368
|
+
const ref = `changed:${f}`;
|
|
369
|
+
if (!handoff.evidence.includes(ref)) {
|
|
370
|
+
handoff.evidence.push(ref);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Current branch
|
|
376
|
+
const branch = _execSync('git branch --show-current 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
377
|
+
if (branch) {
|
|
378
|
+
handoff.evidence.push(`branch:${branch}`);
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
// Git not available — skip enrichment
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Auto-generate pickup_prompt if not manually set
|
|
385
|
+
if (handoff.pickup_prompt === 'none recorded' && summary.totals.calls > 0) {
|
|
386
|
+
const topTier = Object.entries(summary.totals.by_tier)
|
|
387
|
+
.sort(([, a], [, b]) => b - a)[0];
|
|
388
|
+
const tierLabel = topTier ? topTier[0] : 'mixed';
|
|
389
|
+
const fileCount = handoff.evidence.filter(e => e.startsWith('changed:')).length;
|
|
390
|
+
const frictionCount = handoff.friction.length;
|
|
391
|
+
|
|
392
|
+
let prompt = `Session had ${summary.totals.calls} calls (mostly ${tierLabel})`;
|
|
393
|
+
if (fileCount > 0) prompt += `, ${fileCount} files modified`;
|
|
394
|
+
if (frictionCount > 0) prompt += `, ${frictionCount} friction points to review`;
|
|
395
|
+
prompt += '.';
|
|
396
|
+
handoff.pickup_prompt = prompt;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Build the checkpoint object
|
|
400
|
+
const checkpoint = {
|
|
401
|
+
version: 1,
|
|
402
|
+
generated_at: new Date().toISOString(),
|
|
403
|
+
date: summary.date,
|
|
404
|
+
|
|
405
|
+
// Existing summary data
|
|
406
|
+
totals: summary.totals,
|
|
407
|
+
session_insights: summary.session_insights,
|
|
408
|
+
|
|
409
|
+
// New handoff fields
|
|
410
|
+
gate_passed: handoff.gate_passed,
|
|
411
|
+
evidence: handoff.evidence,
|
|
412
|
+
pickup_prompt: handoff.pickup_prompt,
|
|
413
|
+
friction: handoff.friction,
|
|
414
|
+
cross_workstream_patterns: handoff.cross_workstream_patterns,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
return checkpoint;
|
|
418
|
+
}
|
|
419
|
+
|
|
240
420
|
export {
|
|
241
421
|
readSummary,
|
|
242
422
|
updateSummary,
|
|
@@ -246,5 +426,7 @@ export {
|
|
|
246
426
|
getTokenAverages,
|
|
247
427
|
getAdaptiveCodexThreshold,
|
|
248
428
|
updateSessionInsight,
|
|
429
|
+
updateHandoff,
|
|
430
|
+
generateCheckpoint,
|
|
249
431
|
atomicWrite,
|
|
250
432
|
};
|
package/hooks/vibe-router.mjs
CHANGED
|
@@ -140,6 +140,46 @@ function determineQualityGates(tasks) {
|
|
|
140
140
|
return [...gates];
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// ─── Ordered Language Detection ───────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const DEPENDENCY_MARKERS = /\b(then|after\s+that|once\s+\S+\s+is\s+done|before|first|next|finally|afterwards|subsequently|followed\s+by|depends?\s+on|requires?)\b/i;
|
|
146
|
+
|
|
147
|
+
// ─── Subsystem Detection ─────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
const SUBSYSTEM_PATTERNS = [
|
|
150
|
+
{ key: 'auth', regex: /\b(auth|login|sign[-\s]?in|sign[-\s]?up|session|credential|password|oauth|jwt|token)\b/i },
|
|
151
|
+
{ key: 'billing', regex: /\b(billing|payment|subscription|invoice|charge|stripe|pricing)\b/i },
|
|
152
|
+
{ key: 'api', regex: /\b(api|endpoint|route|controller|handler|middleware|rest|graphql)\b/i },
|
|
153
|
+
{ key: 'ui', regex: /\b(ui|nav|button|page|component|layout|style|css|modal|form|menu|sidebar|header|footer|dashboard)\b/i },
|
|
154
|
+
{ key: 'db', regex: /\b(database|db|schema|migration|model|query|table|column|index|sql|prisma|sequelize|knex)\b/i },
|
|
155
|
+
{ key: 'infra', regex: /\b(deploy|ci|cd|docker|k8s|terraform|infra|pipeline|build|config|env)\b/i },
|
|
156
|
+
{ key: 'test', regex: /\b(test|spec|fixture|mock|stub|assert|coverage)\b/i },
|
|
157
|
+
{ key: 'docs', regex: /\b(doc|readme|changelog|guide|tutorial|comment)\b/i },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
function detectSubsystems(text) {
|
|
161
|
+
const subs = new Set();
|
|
162
|
+
for (const pat of SUBSYSTEM_PATTERNS) {
|
|
163
|
+
if (pat.regex.test(text)) subs.add(pat.key);
|
|
164
|
+
}
|
|
165
|
+
return subs;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Risk Domain Extraction ──────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function getRiskDomains(task) {
|
|
171
|
+
const domains = new Set();
|
|
172
|
+
// Use subsystem as risk domain
|
|
173
|
+
const subs = detectSubsystems(task.title);
|
|
174
|
+
for (const s of subs) domains.add(s);
|
|
175
|
+
// Also include explicit risk reason label
|
|
176
|
+
if (task.reason) {
|
|
177
|
+
const match = task.reason.match(/^([^(]+)/);
|
|
178
|
+
if (match) domains.add(match[1].trim().toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
return domains;
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
// ─── Complexity + Wave Recommendation ──────────────────────────────────────
|
|
144
184
|
|
|
145
185
|
function determineComplexity(tasks) {
|
|
@@ -157,18 +197,102 @@ function determineComplexity(tasks) {
|
|
|
157
197
|
return 'simple';
|
|
158
198
|
}
|
|
159
199
|
|
|
160
|
-
|
|
161
|
-
|
|
200
|
+
/**
|
|
201
|
+
* determineWave — Sequential by default, parallel only when tasks are truly independent.
|
|
202
|
+
*
|
|
203
|
+
* Returns { wave, reasons } where reasons is an array of reason codes:
|
|
204
|
+
* shared_surface — tasks likely touch same files
|
|
205
|
+
* high_risk — risky work should be sequential for review
|
|
206
|
+
* dependency_marker — ordered language detected in utterance
|
|
207
|
+
* same_subsystem — tasks in same domain/subsystem
|
|
208
|
+
* independent — truly independent, safe for parallel
|
|
209
|
+
*/
|
|
210
|
+
function determineWave(tasks, complexity, utterance) {
|
|
211
|
+
if (tasks.length === 1) return { wave: 'single', reasons: [] };
|
|
212
|
+
|
|
213
|
+
const reasons = [];
|
|
214
|
+
|
|
215
|
+
// 1. Check for ordered language in the original utterance
|
|
216
|
+
if (utterance && DEPENDENCY_MARKERS.test(utterance)) {
|
|
217
|
+
reasons.push('dependency_marker');
|
|
218
|
+
}
|
|
162
219
|
|
|
163
|
-
//
|
|
164
|
-
// were used), we already split them but keep sequential recommendation.
|
|
165
|
-
// For now, check if tasks share the same tier — parallel is fine for independent work.
|
|
166
|
-
const tiers = new Set(tasks.map(t => t.tier));
|
|
220
|
+
// 2. Check for high/critical risk tasks
|
|
167
221
|
const hasHighRisk = tasks.some(t => t.risk === 'high' || t.risk === 'critical');
|
|
222
|
+
if (hasHighRisk) {
|
|
223
|
+
reasons.push('high_risk');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3. Check for overlapping subsystems between tasks
|
|
227
|
+
const taskSubsystems = tasks.map(t => detectSubsystems(t.title));
|
|
228
|
+
let hasSharedSubsystem = false;
|
|
229
|
+
for (let i = 0; i < taskSubsystems.length; i++) {
|
|
230
|
+
for (let j = i + 1; j < taskSubsystems.length; j++) {
|
|
231
|
+
for (const sub of taskSubsystems[i]) {
|
|
232
|
+
if (taskSubsystems[j].has(sub)) {
|
|
233
|
+
hasSharedSubsystem = true;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (hasSharedSubsystem) break;
|
|
238
|
+
}
|
|
239
|
+
if (hasSharedSubsystem) break;
|
|
240
|
+
}
|
|
241
|
+
if (hasSharedSubsystem) {
|
|
242
|
+
reasons.push('same_subsystem');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 4. Check for overlapping file paths / shared surface area
|
|
246
|
+
const taskPaths = tasks.map(t => extractPaths(t.title));
|
|
247
|
+
let hasSharedPaths = false;
|
|
248
|
+
for (let i = 0; i < taskPaths.length; i++) {
|
|
249
|
+
for (let j = i + 1; j < taskPaths.length; j++) {
|
|
250
|
+
for (const p of taskPaths[i]) {
|
|
251
|
+
// Check if any path from task j shares a directory prefix or exact match
|
|
252
|
+
for (const q of taskPaths[j]) {
|
|
253
|
+
if (p === q || p.startsWith(q + '/') || q.startsWith(p + '/') ||
|
|
254
|
+
p.split('/').slice(0, -1).join('/') === q.split('/').slice(0, -1).join('/')) {
|
|
255
|
+
hasSharedPaths = true;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (hasSharedPaths) break;
|
|
260
|
+
}
|
|
261
|
+
if (hasSharedPaths) break;
|
|
262
|
+
}
|
|
263
|
+
if (hasSharedPaths) break;
|
|
264
|
+
}
|
|
265
|
+
if (hasSharedPaths) {
|
|
266
|
+
reasons.push('shared_surface');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 5. Check for shared risk domains
|
|
270
|
+
const taskDomains = tasks.map(t => getRiskDomains(t));
|
|
271
|
+
let hasSharedDomain = false;
|
|
272
|
+
for (let i = 0; i < taskDomains.length; i++) {
|
|
273
|
+
for (let j = i + 1; j < taskDomains.length; j++) {
|
|
274
|
+
for (const d of taskDomains[i]) {
|
|
275
|
+
if (taskDomains[j].has(d)) {
|
|
276
|
+
hasSharedDomain = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (hasSharedDomain) break;
|
|
281
|
+
}
|
|
282
|
+
if (hasSharedDomain) break;
|
|
283
|
+
}
|
|
284
|
+
// Only add same_subsystem if not already added (risk domains overlap with subsystems)
|
|
285
|
+
if (hasSharedDomain && !reasons.includes('same_subsystem')) {
|
|
286
|
+
reasons.push('same_subsystem');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Decision: parallel ONLY when no sequential reasons found
|
|
290
|
+
if (reasons.length === 0) {
|
|
291
|
+
reasons.push('independent');
|
|
292
|
+
return { wave: 'parallel', reasons };
|
|
293
|
+
}
|
|
168
294
|
|
|
169
|
-
|
|
170
|
-
if (tiers.size === 1 && complexity !== 'complex') return 'parallel';
|
|
171
|
-
return 'parallel';
|
|
295
|
+
return { wave: 'sequential', reasons };
|
|
172
296
|
}
|
|
173
297
|
|
|
174
298
|
// ─── Summary Generation ────────────────────────────────────────────────────
|
|
@@ -229,7 +353,7 @@ function routeVibe(utterance) {
|
|
|
229
353
|
const profileHint = detectProfileHint(utterance);
|
|
230
354
|
const qualityGates = determineQualityGates(tasks);
|
|
231
355
|
const complexity = determineComplexity(tasks);
|
|
232
|
-
const wave = determineWave(tasks, complexity);
|
|
356
|
+
const { wave, reasons } = determineWave(tasks, complexity, utterance);
|
|
233
357
|
const summary = generateSummary(tasks, complexity, wave, qualityGates, profileHint);
|
|
234
358
|
|
|
235
359
|
return {
|
|
@@ -238,6 +362,7 @@ function routeVibe(utterance) {
|
|
|
238
362
|
profile_hint: profileHint,
|
|
239
363
|
quality_gates: qualityGates,
|
|
240
364
|
wave_recommendation: wave,
|
|
365
|
+
wave_reasons: reasons,
|
|
241
366
|
summary,
|
|
242
367
|
};
|
|
243
368
|
}
|
package/package.json
CHANGED