dual-brain 0.2.30 → 0.3.1
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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/precompact.mjs +3 -3
- package/hooks/session-end.mjs +3 -3
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/install.mjs +2 -2
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/observer.mjs
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
|
|
5
|
-
const SEC_PATTERNS = /auth|login|password|token|secret|credential|session|jwt|oauth|permission|role|middleware/i;
|
|
6
|
-
const SOURCE_EXT = /\.(mjs|js|ts|py)$/;
|
|
7
|
-
|
|
8
|
-
function exec(cmd, cwd, timeout = 5000) {
|
|
9
|
-
try {
|
|
10
|
-
return execSync(cmd, { cwd, encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
11
|
-
} catch {
|
|
12
|
-
return '';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function changedFiles(cwd) {
|
|
17
|
-
const output = exec('git diff --name-only HEAD 2>/dev/null || git diff --name-only', cwd);
|
|
18
|
-
return output ? output.split('\n').filter(Boolean) : [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function checkSecurity(files) {
|
|
22
|
-
const hits = files.filter(f => SEC_PATTERNS.test(f));
|
|
23
|
-
if (!hits.length) return null;
|
|
24
|
-
return {
|
|
25
|
-
type: 'security-review',
|
|
26
|
-
priority: 'high',
|
|
27
|
-
message: 'Auth-related files changed — want a security review?',
|
|
28
|
-
action: 'dual-brain review',
|
|
29
|
-
files: hits,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function checkNoTests(files, cwd) {
|
|
34
|
-
const sources = files.filter(f => SOURCE_EXT.test(f));
|
|
35
|
-
if (!sources.length) return null;
|
|
36
|
-
|
|
37
|
-
const untested = sources.filter(f => {
|
|
38
|
-
const base = f.replace(SOURCE_EXT, '');
|
|
39
|
-
const dir = join(cwd, f.split('/').slice(0, -1).join('/'));
|
|
40
|
-
const name = f.split('/').pop().replace(SOURCE_EXT, '');
|
|
41
|
-
const candidates = ['test','spec'].flatMap(k =>
|
|
42
|
-
['mjs','js','ts'].flatMap(e => [
|
|
43
|
-
join(cwd, `${base}.${k}.${e}`),
|
|
44
|
-
join(dir, '__tests__', `${name}.${e}`),
|
|
45
|
-
])
|
|
46
|
-
);
|
|
47
|
-
return !candidates.some(c => existsSync(c));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!untested.length) return null;
|
|
51
|
-
return {
|
|
52
|
-
type: 'no-tests',
|
|
53
|
-
priority: 'medium',
|
|
54
|
-
message: `${untested.length} changed file${untested.length > 1 ? 's' : ''} have no tests`,
|
|
55
|
-
action: "dual-brain go 'add tests for changed files'",
|
|
56
|
-
files: untested,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function checkLargeDiff(cwd) {
|
|
61
|
-
const stat = exec('git diff --stat', cwd);
|
|
62
|
-
if (!stat) return null;
|
|
63
|
-
const match = stat.match(/(\d+) insertion|(\d+) deletion/g);
|
|
64
|
-
if (!match) return null;
|
|
65
|
-
const total = match.reduce((sum, m) => sum + parseInt(m), 0);
|
|
66
|
-
if (total <= 500) return null;
|
|
67
|
-
return {
|
|
68
|
-
type: 'large-diff',
|
|
69
|
-
priority: 'medium',
|
|
70
|
-
message: `Large uncommitted changes (${total} lines) — consider committing`,
|
|
71
|
-
action: "dual-brain go 'commit current changes'",
|
|
72
|
-
files: [],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function checkStaleBranch(cwd, files) {
|
|
77
|
-
if (!files.length) return null;
|
|
78
|
-
const ts = exec('git log -1 --format=%ct', cwd);
|
|
79
|
-
if (!ts) return null;
|
|
80
|
-
const age = Date.now() / 1000 - parseInt(ts);
|
|
81
|
-
if (age < 86400) return null;
|
|
82
|
-
const hours = Math.round(age / 3600);
|
|
83
|
-
return {
|
|
84
|
-
type: 'stale-branch',
|
|
85
|
-
priority: 'low',
|
|
86
|
-
message: `Last commit was ${hours}h ago with uncommitted work`,
|
|
87
|
-
action: "dual-brain go 'commit current changes'",
|
|
88
|
-
files: [],
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function checkConflicts(cwd) {
|
|
93
|
-
const conflicted = exec('git diff --name-only --diff-filter=U', cwd);
|
|
94
|
-
if (!conflicted) return null;
|
|
95
|
-
const files = conflicted.split('\n').filter(Boolean);
|
|
96
|
-
if (!files.length) return null;
|
|
97
|
-
return {
|
|
98
|
-
type: 'conflict',
|
|
99
|
-
priority: 'high',
|
|
100
|
-
message: `${files.length} file${files.length > 1 ? 's' : ''} have merge conflicts`,
|
|
101
|
-
action: "dual-brain go 'resolve merge conflicts'",
|
|
102
|
-
files,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function checkUnfinishedWork(cwd) {
|
|
107
|
-
const outcomesDir = join(cwd, '.dualbrain', 'outcomes');
|
|
108
|
-
if (!existsSync(outcomesDir)) return null;
|
|
109
|
-
|
|
110
|
-
const cutoff = Date.now() - 86_400_000;
|
|
111
|
-
let failed = null;
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const files = readdirSync(outcomesDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
115
|
-
for (const file of files) {
|
|
116
|
-
const lines = readFileSync(join(outcomesDir, file), 'utf8')
|
|
117
|
-
.split('\n').filter(Boolean);
|
|
118
|
-
for (const line of lines.reverse()) {
|
|
119
|
-
try {
|
|
120
|
-
const rec = JSON.parse(line);
|
|
121
|
-
if (rec.timestamp && rec.timestamp < cutoff) break;
|
|
122
|
-
if (rec.result && rec.result.success === false && rec.prompt) {
|
|
123
|
-
failed = rec;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
} catch { /* skip */ }
|
|
127
|
-
}
|
|
128
|
-
if (failed) break;
|
|
129
|
-
}
|
|
130
|
-
} catch { return null; }
|
|
131
|
-
|
|
132
|
-
if (!failed) return null;
|
|
133
|
-
const prompt = failed.prompt.slice(0, 60);
|
|
134
|
-
return {
|
|
135
|
-
type: 'unfinished-work',
|
|
136
|
-
priority: 'medium',
|
|
137
|
-
message: `Last session had a failed task: '${prompt}' — resume?`,
|
|
138
|
-
action: `dual-brain go '${failed.prompt}'`,
|
|
139
|
-
files: [],
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function checkFailingTests(cwd) {
|
|
144
|
-
const pkgPath = join(cwd, 'package.json');
|
|
145
|
-
if (!existsSync(pkgPath)) return null;
|
|
146
|
-
try {
|
|
147
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
148
|
-
if (!pkg.scripts?.test) return null;
|
|
149
|
-
} catch { return null; }
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
execSync('npm test --silent 2>&1', { cwd, encoding: 'utf8', timeout: 30000, stdio: 'pipe' });
|
|
153
|
-
return null;
|
|
154
|
-
} catch {
|
|
155
|
-
return {
|
|
156
|
-
type: 'failing-tests',
|
|
157
|
-
priority: 'high',
|
|
158
|
-
message: 'Tests are failing — want me to investigate?',
|
|
159
|
-
action: "dual-brain go 'fix failing tests'",
|
|
160
|
-
files: [],
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function buildSummary(files, observations) {
|
|
166
|
-
const conflicts = observations.filter(o => o.type === 'conflict').length;
|
|
167
|
-
const hi = observations.filter(o => o.priority === 'high').length;
|
|
168
|
-
const parts = [];
|
|
169
|
-
if (files.length) parts.push(`${files.length} file${files.length > 1 ? 's' : ''} changed`);
|
|
170
|
-
else parts.push('no uncommitted changes');
|
|
171
|
-
if (conflicts) parts.push(`${conflicts} conflict${conflicts > 1 ? 's' : ''}`);
|
|
172
|
-
if (hi) parts.push(`${hi} high-priority suggestion${hi > 1 ? 's' : ''}`);
|
|
173
|
-
return parts.join(', ');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export async function observe(cwd, options = {}) {
|
|
177
|
-
const observations = [];
|
|
178
|
-
try {
|
|
179
|
-
const files = changedFiles(cwd);
|
|
180
|
-
|
|
181
|
-
const sec = checkSecurity(files);
|
|
182
|
-
if (sec) observations.push(sec);
|
|
183
|
-
|
|
184
|
-
const conflicts = checkConflicts(cwd);
|
|
185
|
-
if (conflicts) observations.push(conflicts);
|
|
186
|
-
|
|
187
|
-
const noTests = checkNoTests(files, cwd);
|
|
188
|
-
if (noTests) observations.push(noTests);
|
|
189
|
-
|
|
190
|
-
const largeDiff = checkLargeDiff(cwd);
|
|
191
|
-
if (largeDiff) observations.push(largeDiff);
|
|
192
|
-
|
|
193
|
-
const stale = checkStaleBranch(cwd, files);
|
|
194
|
-
if (stale) observations.push(stale);
|
|
195
|
-
|
|
196
|
-
const unfinished = checkUnfinishedWork(cwd);
|
|
197
|
-
if (unfinished) observations.push(unfinished);
|
|
198
|
-
|
|
199
|
-
if (options.runTests) {
|
|
200
|
-
const failing = await checkFailingTests(cwd);
|
|
201
|
-
if (failing) observations.push(failing);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { observations, summary: buildSummary(files, observations) };
|
|
205
|
-
} catch {
|
|
206
|
-
return { observations: [], summary: 'unable to observe repo state' };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function formatObservations(observations) {
|
|
211
|
-
if (!observations.length) return '💡 Suggestions\n (none)';
|
|
212
|
-
const icon = { high: '🔴', medium: '🟡', low: '🟢' };
|
|
213
|
-
const lines = observations.map(o => ` ${icon[o.priority] || '⚪'} ${o.message}`);
|
|
214
|
-
return `💡 Suggestions\n${lines.join('\n')}`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export async function getQuickState(cwd) {
|
|
218
|
-
try {
|
|
219
|
-
const files = changedFiles(cwd);
|
|
220
|
-
const observations = [];
|
|
221
|
-
|
|
222
|
-
const sec = checkSecurity(files);
|
|
223
|
-
if (sec) observations.push(sec);
|
|
224
|
-
|
|
225
|
-
const conflicts = checkConflicts(cwd);
|
|
226
|
-
if (conflicts) observations.push(conflicts);
|
|
227
|
-
|
|
228
|
-
const noTests = checkNoTests(files, cwd);
|
|
229
|
-
if (noTests) observations.push(noTests);
|
|
230
|
-
|
|
231
|
-
const largeDiff = checkLargeDiff(cwd);
|
|
232
|
-
if (largeDiff) observations.push(largeDiff);
|
|
233
|
-
|
|
234
|
-
const stale = checkStaleBranch(cwd, files);
|
|
235
|
-
if (stale) observations.push(stale);
|
|
236
|
-
|
|
237
|
-
return { observations, summary: buildSummary(files, observations) };
|
|
238
|
-
} catch {
|
|
239
|
-
return { observations: [], summary: 'unable to observe repo state' };
|
|
240
|
-
}
|
|
241
|
-
}
|
package/src/outcome.mjs
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, appendFileSync, writeFileSync, readFileSync, existsSync, readdirSync, unlinkSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { randomUUID } from 'crypto';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
|
-
|
|
6
|
-
const STOP_WORDS = new Set([
|
|
7
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'to', 'from',
|
|
8
|
-
'in', 'on', 'for', 'with', 'and', 'or', 'but', 'not', 'this', 'that', 'it',
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
function outcomesDir(cwd) {
|
|
12
|
-
return join(cwd, '.dualbrain', 'outcomes');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function todayFile(cwd) {
|
|
16
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
17
|
-
return join(outcomesDir(cwd), `outcomes-${date}.jsonl`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function ensureDir(cwd) {
|
|
21
|
-
mkdirSync(outcomesDir(cwd), { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readOutcomeFile(filePath) {
|
|
25
|
-
try {
|
|
26
|
-
return readFileSync(filePath, 'utf8')
|
|
27
|
-
.split('\n')
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
.flatMap(line => {
|
|
30
|
-
try { return [JSON.parse(line)]; } catch { return []; }
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function last7DaysFiles(cwd) {
|
|
38
|
-
const dir = outcomesDir(cwd);
|
|
39
|
-
const files = [];
|
|
40
|
-
for (let i = 0; i < 7; i++) {
|
|
41
|
-
const d = new Date(Date.now() - i * 86_400_000).toISOString().slice(0, 10);
|
|
42
|
-
const f = join(dir, `outcomes-${d}.jsonl`);
|
|
43
|
-
if (existsSync(f)) files.push(f);
|
|
44
|
-
}
|
|
45
|
-
return files;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const INTENT_KEYWORDS = ['implement', 'fix', 'refactor', 'review', 'search', 'test'];
|
|
49
|
-
|
|
50
|
-
function deriveIntent(prompt, tier) {
|
|
51
|
-
const lower = (prompt ?? '').toLowerCase();
|
|
52
|
-
for (const kw of INTENT_KEYWORDS) {
|
|
53
|
-
if (lower.includes(kw)) return kw;
|
|
54
|
-
}
|
|
55
|
-
return tier ?? 'execute';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function pruneOutcomes(cwd) {
|
|
59
|
-
const dir = join(cwd, '.dualbrain', 'outcomes');
|
|
60
|
-
try {
|
|
61
|
-
const files = readdirSync(dir).filter(f => f.startsWith('outcome_')).sort();
|
|
62
|
-
if (files.length > 500) {
|
|
63
|
-
const toDelete = files.slice(0, files.length - 400);
|
|
64
|
-
for (const f of toDelete) {
|
|
65
|
-
try { unlinkSync(join(dir, f)); } catch {}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch {}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function recordDispatchOutcome(dispatchInput, result) {
|
|
72
|
-
try {
|
|
73
|
-
const cwd = dispatchInput.cwd ?? process.cwd();
|
|
74
|
-
const decision = dispatchInput.decision ?? {};
|
|
75
|
-
ensureDir(cwd);
|
|
76
|
-
|
|
77
|
-
const id = `out_${Date.now().toString(36)}`;
|
|
78
|
-
const record = {
|
|
79
|
-
id,
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
prompt: (dispatchInput.prompt ?? '').slice(0, 200),
|
|
82
|
-
tier: decision.tier ?? result.tier ?? 'execute',
|
|
83
|
-
model: decision.model ?? result.model ?? 'unknown',
|
|
84
|
-
provider: decision.provider ?? result.provider ?? 'unknown',
|
|
85
|
-
success: result.status === 'success' || result.status === 'completed',
|
|
86
|
-
status: result.status ?? 'unknown',
|
|
87
|
-
durationMs: result.durationMs ?? 0,
|
|
88
|
-
filesChanged: result.filesChanged?.length ?? 0,
|
|
89
|
-
errors: (result.errors ?? (result.error ? [result.error] : [])).slice(0, 3),
|
|
90
|
-
lesson: '',
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const filePath = join(outcomesDir(cwd), `outcome_${id}.json`);
|
|
94
|
-
writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf8');
|
|
95
|
-
|
|
96
|
-
// Score the outcome for the routing advisor (non-blocking)
|
|
97
|
-
try {
|
|
98
|
-
import('./signal.mjs').then(({ scoreOutcome }) =>
|
|
99
|
-
import('./routing-advisor.mjs').then(({ recordReward }) => {
|
|
100
|
-
const scored = scoreOutcome(record);
|
|
101
|
-
const intent = deriveIntent(record.prompt, record.tier);
|
|
102
|
-
const cellKey = `${record.tier}:${intent}`;
|
|
103
|
-
// Normalize full model ID to short name for the advisor cell
|
|
104
|
-
const modelId = record.model ?? 'sonnet';
|
|
105
|
-
const shortModel = /haiku/i.test(modelId) ? 'haiku'
|
|
106
|
-
: /opus/i.test(modelId) ? 'opus'
|
|
107
|
-
: 'sonnet';
|
|
108
|
-
recordReward(cellKey, shortModel, scored.reward, cwd);
|
|
109
|
-
})
|
|
110
|
-
).catch(() => { /* non-blocking */ });
|
|
111
|
-
} catch { /* non-blocking */ }
|
|
112
|
-
|
|
113
|
-
pruneOutcomes(cwd);
|
|
114
|
-
return record;
|
|
115
|
-
} catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function computeRoutingScore(plan, result, verification) {
|
|
121
|
-
let score = 3;
|
|
122
|
-
if (result.success && result.duration < 60_000) score += 1;
|
|
123
|
-
if (verification.filesVerified && verification.testsPassed === true) score += 1;
|
|
124
|
-
if (result.error) score -= 1;
|
|
125
|
-
if (result.duration > 180_000) score -= 1;
|
|
126
|
-
if ((plan.challengerPolicy === 'none' || !plan.challengerPolicy) && !result.success) score -= 2;
|
|
127
|
-
return Math.max(1, Math.min(5, score));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function generateLessons(plan, result, verification) {
|
|
131
|
-
const lessons = [];
|
|
132
|
-
const noChallenger = !plan.challengerPolicy || plan.challengerPolicy === 'none';
|
|
133
|
-
|
|
134
|
-
if (noChallenger && !result.success) {
|
|
135
|
-
lessons.push('Task failed without challenger — consider escalating similar tasks');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
plan.reasoningDepth === 'ultra' &&
|
|
140
|
-
result.duration < 60_000 &&
|
|
141
|
-
(plan.complexity === 'simple' || plan.complexity === 'low')
|
|
142
|
-
) {
|
|
143
|
-
lessons.push('Ultra reasoning unnecessary — task completed quickly at low complexity');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!result.success) {
|
|
147
|
-
const keywords = (plan.prompt || '')
|
|
148
|
-
.toLowerCase()
|
|
149
|
-
.split(/\s+/)
|
|
150
|
-
.filter(w => w.length > 3 && !STOP_WORDS.has(w))
|
|
151
|
-
.slice(0, 4)
|
|
152
|
-
.join(' ');
|
|
153
|
-
if (keywords) {
|
|
154
|
-
lessons.push(`Prior failure pattern: ${keywords} on ${plan.tier}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!noChallenger && result.success && verification.filesVerified) {
|
|
159
|
-
lessons.push(`Challenger caught issues — keep challenger policy for ${plan.risk} risk`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return lessons;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function recordOutcome(plan, result, verification, cwd) {
|
|
166
|
-
try {
|
|
167
|
-
ensureDir(cwd);
|
|
168
|
-
|
|
169
|
-
const routingScore = computeRoutingScore(plan, result, verification);
|
|
170
|
-
const lessons = generateLessons(plan, result, verification);
|
|
171
|
-
|
|
172
|
-
const record = {
|
|
173
|
-
id: randomUUID(),
|
|
174
|
-
timestamp: Date.now(),
|
|
175
|
-
prompt: plan.prompt ?? '',
|
|
176
|
-
tier: plan.tier ?? '',
|
|
177
|
-
primaryModel: plan.primaryModel ?? '',
|
|
178
|
-
reasoningDepth: plan.reasoningDepth ?? '',
|
|
179
|
-
challengerPolicy: plan.challengerPolicy ?? 'none',
|
|
180
|
-
risk: plan.risk ?? '',
|
|
181
|
-
result: {
|
|
182
|
-
success: result.success ?? false,
|
|
183
|
-
filesChanged: result.filesChanged ?? [],
|
|
184
|
-
duration: result.duration ?? 0,
|
|
185
|
-
error: result.error ?? null,
|
|
186
|
-
},
|
|
187
|
-
verification: {
|
|
188
|
-
filesVerified: verification.filesVerified ?? false,
|
|
189
|
-
testsRun: verification.testsRun ?? false,
|
|
190
|
-
testsPassed: verification.testsPassed ?? null,
|
|
191
|
-
},
|
|
192
|
-
routingScore,
|
|
193
|
-
lessons,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
appendFileSync(todayFile(cwd), JSON.stringify(record) + '\n', 'utf8');
|
|
197
|
-
return record;
|
|
198
|
-
} catch {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function tokenize(text) {
|
|
204
|
-
return (text || '')
|
|
205
|
-
.toLowerCase()
|
|
206
|
-
.split(/\W+/)
|
|
207
|
-
.filter(w => w.length > 3 && !STOP_WORDS.has(w));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function promptOverlap(a, b) {
|
|
211
|
-
const wordsA = new Set(tokenize(a));
|
|
212
|
-
const wordsB = tokenize(b);
|
|
213
|
-
return wordsB.filter(w => wordsA.has(w)).length;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function fileOverlap(filesA = [], filesB = []) {
|
|
217
|
-
const setA = new Set(filesA.map(f => f.split('/').pop()));
|
|
218
|
-
return filesB.map(f => f.split('/').pop()).filter(f => setA.has(f)).length;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export async function getRelevantOutcomes(prompt, files = [], cwd, options = {}) {
|
|
222
|
-
try {
|
|
223
|
-
const allFiles = last7DaysFiles(cwd);
|
|
224
|
-
const outcomes = allFiles.flatMap(readOutcomeFile);
|
|
225
|
-
|
|
226
|
-
const scored = outcomes.map(o => {
|
|
227
|
-
let score = promptOverlap(prompt, o.prompt);
|
|
228
|
-
score += fileOverlap(files, o.result?.filesChanged ?? []);
|
|
229
|
-
return { o, score };
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
return scored
|
|
233
|
-
.filter(({ score }) => score >= 2)
|
|
234
|
-
.sort((a, b) => b.score - a.score)
|
|
235
|
-
.slice(0, 5)
|
|
236
|
-
.map(({ o, score }) => ({
|
|
237
|
-
id: o.id,
|
|
238
|
-
timestamp: o.timestamp,
|
|
239
|
-
prompt: o.prompt,
|
|
240
|
-
success: o.result?.success ?? false,
|
|
241
|
-
routingScore: o.routingScore,
|
|
242
|
-
lessons: o.lessons,
|
|
243
|
-
relevanceScore: score,
|
|
244
|
-
}));
|
|
245
|
-
} catch {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export async function checkFileSurvival(cwd) {
|
|
251
|
-
try {
|
|
252
|
-
const dir = join(cwd, '.dualbrain', 'outcomes');
|
|
253
|
-
if (!existsSync(dir)) return [];
|
|
254
|
-
|
|
255
|
-
// Collect up to the last 20 individual outcome JSON files
|
|
256
|
-
let files;
|
|
257
|
-
try {
|
|
258
|
-
files = readdirSync(dir)
|
|
259
|
-
.filter(f => f.startsWith('outcome_') && f.endsWith('.json'))
|
|
260
|
-
.sort()
|
|
261
|
-
.slice(-20);
|
|
262
|
-
} catch {
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Get current git-modified files (best-effort)
|
|
267
|
-
let modifiedFiles = new Set();
|
|
268
|
-
try {
|
|
269
|
-
const gitOut = execSync('git diff --name-only', { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
270
|
-
for (const f of gitOut.split('\n').map(l => l.trim()).filter(Boolean)) {
|
|
271
|
-
modifiedFiles.add(f);
|
|
272
|
-
modifiedFiles.add(join(cwd, f));
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
// git unavailable — proceed without modified-file check
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const scored = [];
|
|
279
|
-
|
|
280
|
-
for (const fname of files) {
|
|
281
|
-
const fpath = join(dir, fname);
|
|
282
|
-
let record;
|
|
283
|
-
try {
|
|
284
|
-
record = JSON.parse(readFileSync(fpath, 'utf8'));
|
|
285
|
-
} catch {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Skip if already scored or no filesChanged list
|
|
290
|
-
if (record.survivalScore !== undefined) continue;
|
|
291
|
-
const changedFiles = record.result?.filesChanged;
|
|
292
|
-
if (!Array.isArray(changedFiles) || changedFiles.length === 0) continue;
|
|
293
|
-
|
|
294
|
-
let survived = 0;
|
|
295
|
-
for (const f of changedFiles) {
|
|
296
|
-
const absPath = f.startsWith('/') ? f : join(cwd, f);
|
|
297
|
-
const exists = existsSync(absPath);
|
|
298
|
-
const modified = modifiedFiles.has(f) || modifiedFiles.has(absPath);
|
|
299
|
-
if (exists && !modified) survived++;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const survivalScore = survived / changedFiles.length;
|
|
303
|
-
record.survivalScore = survivalScore;
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
writeFileSync(fpath, JSON.stringify(record, null, 2), 'utf8');
|
|
307
|
-
} catch {
|
|
308
|
-
// write failed — skip
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
scored.push({ id: record.id, survivalScore });
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return scored;
|
|
316
|
-
} catch {
|
|
317
|
-
return [];
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export async function getOutcomeStats(cwd, days = 7) {
|
|
322
|
-
try {
|
|
323
|
-
const allFiles = last7DaysFiles(cwd).slice(0, days);
|
|
324
|
-
const outcomes = allFiles.flatMap(readOutcomeFile);
|
|
325
|
-
|
|
326
|
-
if (outcomes.length === 0) {
|
|
327
|
-
return {
|
|
328
|
-
totalTasks: 0,
|
|
329
|
-
successRate: 0,
|
|
330
|
-
avgRoutingScore: 0,
|
|
331
|
-
avgDuration: 0,
|
|
332
|
-
challengerHelpRate: 0,
|
|
333
|
-
topLessons: [],
|
|
334
|
-
modelBreakdown: {},
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const totalTasks = outcomes.length;
|
|
339
|
-
const successes = outcomes.filter(o => o.result?.success).length;
|
|
340
|
-
const successRate = successes / totalTasks;
|
|
341
|
-
|
|
342
|
-
const avgRoutingScore =
|
|
343
|
-
outcomes.reduce((sum, o) => sum + (o.routingScore ?? 3), 0) / totalTasks;
|
|
344
|
-
|
|
345
|
-
const avgDuration =
|
|
346
|
-
outcomes.reduce((sum, o) => sum + (o.result?.duration ?? 0), 0) / totalTasks;
|
|
347
|
-
|
|
348
|
-
const challengerUsed = outcomes.filter(
|
|
349
|
-
o => o.challengerPolicy && o.challengerPolicy !== 'none'
|
|
350
|
-
);
|
|
351
|
-
const challengerHelped = challengerUsed.filter(o => o.result?.success);
|
|
352
|
-
const challengerHelpRate =
|
|
353
|
-
challengerUsed.length > 0 ? challengerHelped.length / challengerUsed.length : 0;
|
|
354
|
-
|
|
355
|
-
const lessonCounts = {};
|
|
356
|
-
for (const o of outcomes) {
|
|
357
|
-
for (const lesson of o.lessons ?? []) {
|
|
358
|
-
lessonCounts[lesson] = (lessonCounts[lesson] ?? 0) + 1;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
const topLessons = Object.entries(lessonCounts)
|
|
362
|
-
.sort((a, b) => b[1] - a[1])
|
|
363
|
-
.slice(0, 5)
|
|
364
|
-
.map(([lesson]) => lesson);
|
|
365
|
-
|
|
366
|
-
const modelBreakdown = {};
|
|
367
|
-
for (const o of outcomes) {
|
|
368
|
-
const model = o.primaryModel;
|
|
369
|
-
if (!model) continue;
|
|
370
|
-
if (!modelBreakdown[model]) modelBreakdown[model] = { count: 0, successCount: 0 };
|
|
371
|
-
modelBreakdown[model].count += 1;
|
|
372
|
-
if (o.result?.success) modelBreakdown[model].successCount += 1;
|
|
373
|
-
}
|
|
374
|
-
for (const model of Object.keys(modelBreakdown)) {
|
|
375
|
-
const { count, successCount } = modelBreakdown[model];
|
|
376
|
-
modelBreakdown[model].successRate = count > 0 ? successCount / count : 0;
|
|
377
|
-
delete modelBreakdown[model].successCount;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
totalTasks,
|
|
382
|
-
successRate,
|
|
383
|
-
avgRoutingScore,
|
|
384
|
-
avgDuration,
|
|
385
|
-
challengerHelpRate,
|
|
386
|
-
topLessons,
|
|
387
|
-
modelBreakdown,
|
|
388
|
-
};
|
|
389
|
-
} catch {
|
|
390
|
-
return {
|
|
391
|
-
totalTasks: 0,
|
|
392
|
-
successRate: 0,
|
|
393
|
-
avgRoutingScore: 0,
|
|
394
|
-
avgDuration: 0,
|
|
395
|
-
challengerHelpRate: 0,
|
|
396
|
-
topLessons: [],
|
|
397
|
-
modelBreakdown: {},
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
}
|