dual-brain 4.0.0 → 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/CLAUDE.md +33 -2
- package/README.md +26 -1
- 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/CLAUDE.md
CHANGED
|
@@ -64,18 +64,49 @@ Profile persists to `.claude/dual-brain.profile.json` (gitignored).
|
|
|
64
64
|
Switch profiles: `npx dual-brain mode cost-saver`
|
|
65
65
|
Check status: `npx dual-brain status`
|
|
66
66
|
|
|
67
|
+
Natural language aliases work everywhere: "go aggressive", "be careful", "cheap mode", "fast", "thorough", "smart". The system strips prefixes like "go"/"be"/"use" and resolves to the canonical profile name.
|
|
68
|
+
|
|
67
69
|
## Adaptive Routing (Auto Mode)
|
|
68
70
|
|
|
69
71
|
Auto mode classifies risk from file paths and adjusts routing in real-time:
|
|
70
72
|
|
|
71
73
|
- **Risk classification**: auth/secrets→critical, billing/migrations→high, tests/utils→medium, docs→low
|
|
72
|
-
- **Failure detection**: 2+ failures on same prompt in 2 hours → auto-escalate tier or trigger dual-brain
|
|
74
|
+
- **Failure detection**: 2+ failures on same prompt in 2 hours → auto-escalate tier or trigger dual-brain. Uses time-weighted decay (recent failures count more) and ledger pruning for entries >24hrs.
|
|
73
75
|
- **Provider balance**: Routes to underused provider when one subscription is hot
|
|
76
|
+
- **Burst awareness**: Suppresses duplicate warnings and balance hints during agent waves (3+ agents in 90s)
|
|
77
|
+
|
|
78
|
+
## Vibe Coding
|
|
79
|
+
|
|
80
|
+
Casual natural language → structured work. The vibe coding system translates informal requests into properly routed, risk-classified, quality-gated work.
|
|
81
|
+
|
|
82
|
+
**Intent compiler** — decompose multi-task requests:
|
|
83
|
+
```bash
|
|
84
|
+
node .claude/hooks/vibe-router.mjs "fix the login bug and also update the nav"
|
|
85
|
+
```
|
|
86
|
+
Returns structured tasks with tier/risk classification, complexity level, quality gates, and wave strategy.
|
|
87
|
+
|
|
88
|
+
**Plan generator** — Steve-style 3-part markdown plans:
|
|
89
|
+
```bash
|
|
90
|
+
node .claude/hooks/plan-generator.mjs --utterance "..." [--write]
|
|
91
|
+
```
|
|
92
|
+
Generates: (1) dependency-ordered task table, (2) user stories + edge cases, (3) questions with suggested answers. Pass `--write` to save to `.claude/plans/`.
|
|
93
|
+
|
|
94
|
+
**Durable memory** — preferences persist across sessions:
|
|
95
|
+
```bash
|
|
96
|
+
node .claude/hooks/vibe-memory.mjs # show state
|
|
97
|
+
node .claude/hooks/vibe-memory.mjs --set preferences.risk_tolerance=careful
|
|
98
|
+
node .claude/hooks/vibe-memory.mjs --threads # active work
|
|
99
|
+
node .claude/hooks/vibe-memory.mjs --infer # preference suggestions
|
|
100
|
+
```
|
|
101
|
+
Tracks preferred profile, risk tolerance, active threads, and learns from usage patterns.
|
|
74
102
|
|
|
75
103
|
## Available Tools
|
|
76
104
|
|
|
105
|
+
- `node .claude/hooks/vibe-router.mjs "..."` — decompose casual requests into structured work
|
|
106
|
+
- `node .claude/hooks/plan-generator.mjs --utterance "..."` — generate execution plans
|
|
107
|
+
- `node .claude/hooks/vibe-memory.mjs` — persistent preferences and work threads
|
|
77
108
|
- `node .claude/hooks/cost-report.mjs` — activity and cost estimates
|
|
78
109
|
- `node .claude/hooks/health-check.mjs` — verify system health
|
|
79
110
|
- `node .claude/hooks/budget-balancer.mjs` — provider balance status
|
|
80
111
|
- `node .claude/hooks/decision-ledger.mjs` — routing outcome insights
|
|
81
|
-
- `node .claude/hooks/test-orchestrator.mjs` — run self-tests
|
|
112
|
+
- `node .claude/hooks/test-orchestrator.mjs` — run self-tests (40 tests)
|
package/README.md
CHANGED
|
@@ -51,10 +51,35 @@ npx -y dual-brain
|
|
|
51
51
|
|
|
52
52
|
**Dual-brain** is recommended automatically for high-risk decisions — hooks detect the risk level and suggest dual-brain analysis, where both providers think on the same problem independently.
|
|
53
53
|
|
|
54
|
+
## Vibe Coding
|
|
55
|
+
|
|
56
|
+
Speak naturally. The orchestrator handles the structure.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Decompose a casual request into structured work
|
|
60
|
+
node .claude/hooks/vibe-router.mjs "fix the login bug and also update the nav"
|
|
61
|
+
|
|
62
|
+
# Generate a Steve-style execution plan
|
|
63
|
+
node .claude/hooks/plan-generator.mjs --utterance "refactor the auth flow" --write
|
|
64
|
+
|
|
65
|
+
# Switch profiles with natural language
|
|
66
|
+
npx dual-brain mode "go aggressive"
|
|
67
|
+
npx dual-brain mode "be careful"
|
|
68
|
+
npx dual-brain mode "cheap"
|
|
69
|
+
|
|
70
|
+
# Check persistent preferences and work threads
|
|
71
|
+
node .claude/hooks/vibe-memory.mjs --threads
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The vibe-router splits multi-task requests, classifies risk, assigns tiers, and recommends quality gates. The plan-generator produces 3-part plans (dependency-ordered tasks, user stories, questions with suggested answers). Vibe-memory learns your preferences over time.
|
|
75
|
+
|
|
54
76
|
## Scripts
|
|
55
77
|
|
|
56
78
|
| Script | Purpose |
|
|
57
79
|
|--------|---------|
|
|
80
|
+
| `hooks/vibe-router.mjs` | Decompose casual language into structured work orders |
|
|
81
|
+
| `hooks/plan-generator.mjs` | Generate Steve-style 3-part execution plans |
|
|
82
|
+
| `hooks/vibe-memory.mjs` | Persistent preferences, work threads, preference inference |
|
|
58
83
|
| `hooks/cost-report.mjs` | Activity & cost estimates by model tier |
|
|
59
84
|
| `hooks/dual-brain-review.mjs` | Send git diff to GPT for independent review |
|
|
60
85
|
| `hooks/dual-brain-think.mjs` | Dual-perspective analysis on architecture decisions |
|
|
@@ -63,7 +88,7 @@ npx -y dual-brain
|
|
|
63
88
|
| `hooks/gpt-work-dispatcher.mjs` | Dispatch execution tasks to GPT via Codex CLI |
|
|
64
89
|
| `hooks/session-report.mjs` | Session-end summary: activity, compliance, quality |
|
|
65
90
|
| `hooks/health-check.mjs` | Verify all hooks and dependencies are working |
|
|
66
|
-
| `hooks/test-orchestrator.mjs` | Self-test harness (
|
|
91
|
+
| `hooks/test-orchestrator.mjs` | Self-test harness (40 tests) |
|
|
67
92
|
| `hooks/setup-wizard.mjs` | Interactive config (optional — for custom plans) |
|
|
68
93
|
| `hooks/install-git-hooks.mjs` | Git pre-commit hook for quality gate |
|
|
69
94
|
|
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