dual-brain 0.2.30 → 0.3.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/.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/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- 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/governance.mjs
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
// governance.mjs — Model tier enforcement + multi-model collaboration
|
|
2
|
-
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
// ─── Tier Definitions ────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
export const MODEL_TIERS = Object.freeze({
|
|
9
|
-
1: {
|
|
10
|
-
label: 'lightweight',
|
|
11
|
-
models: ['claude-haiku-4-5-20251001', 'haiku', 'gpt-4o-mini', 'o4-mini'],
|
|
12
|
-
autoApprove: true,
|
|
13
|
-
},
|
|
14
|
-
2: {
|
|
15
|
-
label: 'standard',
|
|
16
|
-
models: ['claude-sonnet-4-6', 'sonnet', 'gpt-4o', 'gpt-4.1'],
|
|
17
|
-
autoApprove: true,
|
|
18
|
-
},
|
|
19
|
-
3: {
|
|
20
|
-
label: 'heavy',
|
|
21
|
-
models: ['claude-opus-4-6', 'claude-opus-4-7', 'opus', 'o3'],
|
|
22
|
-
autoApprove: false, // requires consent check per profile
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Reverse lookup: model ID → tier number
|
|
27
|
-
export function getModelTier(modelId) {
|
|
28
|
-
if (!modelId) return 2; // default to standard
|
|
29
|
-
const normalized = String(modelId).toLowerCase();
|
|
30
|
-
for (const [tier, def] of Object.entries(MODEL_TIERS)) {
|
|
31
|
-
if (def.models.some(m => normalized.includes(m))) return Number(tier);
|
|
32
|
-
}
|
|
33
|
-
return 2; // unknown models default to standard
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ─── Task Scoring ────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
export function scoreTask(detection) {
|
|
39
|
-
// detection comes from detect.mjs — has intent, risk, scope, files, etc.
|
|
40
|
-
const scores = {
|
|
41
|
-
complexity: 0, // 0-3
|
|
42
|
-
risk: 0, // 0-3
|
|
43
|
-
creativity: 0, // 0-2
|
|
44
|
-
precision: 0, // 0-2
|
|
45
|
-
contextVolume: 0, // 0-3
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Complexity from file count / scope
|
|
49
|
-
const fileCount = detection?.files?.length || detection?.scope?.fileCount || 0;
|
|
50
|
-
if (fileCount >= 6) scores.complexity = 3;
|
|
51
|
-
else if (fileCount >= 3) scores.complexity = 2;
|
|
52
|
-
else if (fileCount >= 1) scores.complexity = 1;
|
|
53
|
-
|
|
54
|
-
// Risk from explicit risk field or keywords
|
|
55
|
-
const risk = detection?.risk || detection?.riskLevel || 'low';
|
|
56
|
-
const riskMap = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
57
|
-
scores.risk = riskMap[risk] ?? 0;
|
|
58
|
-
|
|
59
|
-
// Boost risk for security/auth/billing keywords
|
|
60
|
-
const text = (detection?.objective || detection?.intent || '').toLowerCase();
|
|
61
|
-
if (/\b(auth|security|credential|secret|billing|payment|migration|delete|drop)\b/.test(text)) {
|
|
62
|
-
scores.risk = Math.max(scores.risk, 2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Creativity from intent type
|
|
66
|
-
const intent = (detection?.intent || detection?.type || '').toLowerCase();
|
|
67
|
-
if (/\b(architect|design|brainstorm|explore|research)\b/.test(intent)) scores.creativity = 2;
|
|
68
|
-
else if (/\b(refactor|plan|decide)\b/.test(intent)) scores.creativity = 1;
|
|
69
|
-
|
|
70
|
-
// Precision — one-shot tasks need higher precision
|
|
71
|
-
if (/\b(security|deploy|publish|migration)\b/.test(text)) scores.precision = 2;
|
|
72
|
-
else if (/\b(implement|build|create)\b/.test(text)) scores.precision = 1;
|
|
73
|
-
|
|
74
|
-
// Context volume
|
|
75
|
-
const contextSize = detection?.contextTokens || detection?.estimatedContext || 0;
|
|
76
|
-
if (contextSize > 200000) scores.contextVolume = 3;
|
|
77
|
-
else if (contextSize > 50000) scores.contextVolume = 2;
|
|
78
|
-
else if (contextSize > 10000) scores.contextVolume = 1;
|
|
79
|
-
|
|
80
|
-
return scores;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function computeRequiredTier(scores) {
|
|
84
|
-
const total = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
85
|
-
if (total <= 2) return 1;
|
|
86
|
-
if (total <= 6) return 2;
|
|
87
|
-
return 3;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ─── Governance Assessment ───────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
// Profile governance defaults
|
|
93
|
-
const GOVERNANCE_PERMISSIONS = {
|
|
94
|
-
'auto': { 1: 'auto', 2: 'auto', 3: 'ask' },
|
|
95
|
-
'balanced': { 1: 'auto', 2: 'auto', 3: 'ask' },
|
|
96
|
-
'cost-saver': { 1: 'auto', 2: 'auto', 3: 'deny' },
|
|
97
|
-
'quality-first': { 1: 'auto', 2: 'auto', 3: 'auto' },
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Pricing per million tokens (input/output) for cost estimation
|
|
101
|
-
const MODEL_PRICING = {
|
|
102
|
-
'haiku': { input: 1.00, output: 5.00 },
|
|
103
|
-
'sonnet': { input: 3.00, output: 15.00 },
|
|
104
|
-
'opus': { input: 5.00, output: 25.00 },
|
|
105
|
-
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
106
|
-
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
107
|
-
'gpt-4.1': { input: 2.00, output: 8.00 },
|
|
108
|
-
'o3': { input: 2.00, output: 8.00 },
|
|
109
|
-
'o4-mini': { input: 1.10, output: 4.40 },
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
function estimateCost(modelId, estimatedTokens = 8000) {
|
|
113
|
-
const normalized = String(modelId).toLowerCase();
|
|
114
|
-
let pricing = MODEL_PRICING['sonnet']; // default
|
|
115
|
-
for (const [key, p] of Object.entries(MODEL_PRICING)) {
|
|
116
|
-
if (normalized.includes(key)) { pricing = p; break; }
|
|
117
|
-
}
|
|
118
|
-
// Assume 20% input, 80% output for agent tasks
|
|
119
|
-
const inputTokens = estimatedTokens * 0.2;
|
|
120
|
-
const outputTokens = estimatedTokens * 0.8;
|
|
121
|
-
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function assessGovernance(model, detection, profile) {
|
|
125
|
-
const tier = getModelTier(model);
|
|
126
|
-
const scores = scoreTask(detection);
|
|
127
|
-
const requiredTier = computeRequiredTier(scores);
|
|
128
|
-
const workStyle = profile?.workStyle || profile?.name || 'auto';
|
|
129
|
-
const permissions = GOVERNANCE_PERMISSIONS[workStyle] || GOVERNANCE_PERMISSIONS['auto'];
|
|
130
|
-
const permission = permissions[tier] || 'ask';
|
|
131
|
-
const estimatedCost = estimateCost(model);
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
requestedTier: tier,
|
|
135
|
-
requiredTier,
|
|
136
|
-
overProvisioned: tier > requiredTier,
|
|
137
|
-
underProvisioned: tier < requiredTier,
|
|
138
|
-
permission, // 'auto' | 'ask' | 'deny'
|
|
139
|
-
estimatedCost,
|
|
140
|
-
scores,
|
|
141
|
-
justification: buildJustification(scores, tier, requiredTier),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function buildJustification(scores, requestedTier, requiredTier) {
|
|
146
|
-
const parts = [];
|
|
147
|
-
if (scores.risk >= 2) parts.push('high-risk');
|
|
148
|
-
if (scores.complexity >= 2) parts.push('complex');
|
|
149
|
-
if (scores.creativity >= 2) parts.push('creative/architectural');
|
|
150
|
-
if (scores.contextVolume >= 2) parts.push('large-context');
|
|
151
|
-
if (requestedTier > requiredTier) parts.push('over-provisioned');
|
|
152
|
-
if (requestedTier < requiredTier) parts.push('under-provisioned');
|
|
153
|
-
return parts.join(', ') || 'standard task';
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ─── Collaboration Assessment ────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
export function shouldCollaborate(detection, governance, profile) {
|
|
159
|
-
// Never collaborate on tier-1 tasks
|
|
160
|
-
if (governance.requiredTier <= 1) return { collaborate: false };
|
|
161
|
-
|
|
162
|
-
// Never collaborate in cost-saver mode unless explicitly requested
|
|
163
|
-
const workStyle = profile?.workStyle || profile?.name || 'auto';
|
|
164
|
-
if (workStyle === 'cost-saver') return { collaborate: false };
|
|
165
|
-
|
|
166
|
-
// Check collaboration triggers (need ANY two)
|
|
167
|
-
const triggers = [];
|
|
168
|
-
const text = (detection?.objective || detection?.intent || '').toLowerCase();
|
|
169
|
-
|
|
170
|
-
if (/\b(auth|security|credential|billing|migration)\b/.test(text)) triggers.push('irreversibility');
|
|
171
|
-
if (detection?.ambiguity === 'high' || /\b(should we|how to|best approach|tradeoff)\b/.test(text)) triggers.push('ambiguity');
|
|
172
|
-
if (detection?.novelty === 'high' || /\b(new|first time|never done|greenfield)\b/.test(text)) triggers.push('novelty');
|
|
173
|
-
if ((detection?.files?.length || 0) >= 4 && /\b(security|performance|ux)\b/.test(text)) triggers.push('cross-domain');
|
|
174
|
-
if (governance.requestedTier >= 3 && detection?.confidence && detection.confidence < 0.8) triggers.push('low-confidence');
|
|
175
|
-
|
|
176
|
-
const shouldDo = triggers.length >= 2;
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
collaborate: shouldDo,
|
|
180
|
-
triggers,
|
|
181
|
-
pattern: shouldDo ? selectPattern(triggers, detection) : null,
|
|
182
|
-
estimatedOverhead: shouldDo ? estimateCost('gpt-4.1') : 0, // secondary model cost
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function selectPattern(triggers, detection) {
|
|
187
|
-
const text = (detection?.objective || detection?.intent || '').toLowerCase();
|
|
188
|
-
|
|
189
|
-
// Security → adversarial review
|
|
190
|
-
if (triggers.includes('irreversibility') && /\b(auth|security|credential)\b/.test(text)) {
|
|
191
|
-
return 'adversarial-review';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Architecture/greenfield → second opinion (perspective rotation reserved for Phase 4)
|
|
195
|
-
if (triggers.includes('novelty') || triggers.includes('ambiguity')) {
|
|
196
|
-
return 'second-opinion';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Default
|
|
200
|
-
return 'second-opinion';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ─── Governance State (Session Budget Tracking) ──────────────────────────────
|
|
204
|
-
|
|
205
|
-
const STATE_FILE = '.dualbrain/governance-state.json';
|
|
206
|
-
const SESSION_GAP_MS = 30 * 60 * 1000; // 30 min gap = new session
|
|
207
|
-
|
|
208
|
-
export function loadGovernanceState(cwd) {
|
|
209
|
-
const statePath = join(cwd, STATE_FILE);
|
|
210
|
-
try {
|
|
211
|
-
const raw = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
212
|
-
// Check if session is stale
|
|
213
|
-
const lastDispatch = raw.dispatches?.[raw.dispatches.length - 1];
|
|
214
|
-
if (lastDispatch && (Date.now() - Date.parse(lastDispatch.ts)) > SESSION_GAP_MS) {
|
|
215
|
-
// Stale session — reset
|
|
216
|
-
return freshState();
|
|
217
|
-
}
|
|
218
|
-
return raw;
|
|
219
|
-
} catch {
|
|
220
|
-
return freshState();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function freshState() {
|
|
225
|
-
return {
|
|
226
|
-
sessionStartedAt: new Date().toISOString(),
|
|
227
|
-
dispatches: [],
|
|
228
|
-
totalEstimatedCost: 0,
|
|
229
|
-
tierCounts: { 1: 0, 2: 0, 3: 0 },
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function recordDispatch(cwd, tier, model, estimatedCost, approved = true) {
|
|
234
|
-
const state = loadGovernanceState(cwd);
|
|
235
|
-
state.dispatches.push({
|
|
236
|
-
tier,
|
|
237
|
-
model: String(model),
|
|
238
|
-
estimatedCost,
|
|
239
|
-
approved,
|
|
240
|
-
ts: new Date().toISOString(),
|
|
241
|
-
});
|
|
242
|
-
state.totalEstimatedCost += estimatedCost;
|
|
243
|
-
state.tierCounts[tier] = (state.tierCounts[tier] || 0) + 1;
|
|
244
|
-
|
|
245
|
-
const dir = join(cwd, '.dualbrain');
|
|
246
|
-
mkdirSync(dir, { recursive: true });
|
|
247
|
-
writeFileSync(join(cwd, STATE_FILE), JSON.stringify(state, null, 2) + '\n');
|
|
248
|
-
return state;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function checkBudget(cwd, orchestratorConfig) {
|
|
252
|
-
const state = loadGovernanceState(cwd);
|
|
253
|
-
const sessionLimit = orchestratorConfig?.budgets?.session_limit_usd || 10;
|
|
254
|
-
const remaining = sessionLimit - state.totalEstimatedCost;
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
spent: state.totalEstimatedCost,
|
|
258
|
-
remaining,
|
|
259
|
-
limit: sessionLimit,
|
|
260
|
-
warning: remaining < sessionLimit * 0.2, // <20% remaining
|
|
261
|
-
blocked: remaining <= 0,
|
|
262
|
-
tierCounts: state.tierCounts,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ─── Format for User Display ─────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
export function formatGovernancePrompt(governance, collaboration) {
|
|
269
|
-
const tierLabel = MODEL_TIERS[governance.requestedTier]?.label || 'unknown';
|
|
270
|
-
const lines = [];
|
|
271
|
-
|
|
272
|
-
lines.push(`[governance] Task requires ${tierLabel} model (tier ${governance.requestedTier}, ~$${governance.estimatedCost.toFixed(2)})`);
|
|
273
|
-
if (governance.justification) lines.push(` Reason: ${governance.justification}`);
|
|
274
|
-
if (collaboration?.collaborate) {
|
|
275
|
-
lines.push(` + ${collaboration.pattern} with secondary model (+$${collaboration.estimatedOverhead.toFixed(2)})`);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return lines.join('\n');
|
|
279
|
-
}
|
package/src/handoff.mjs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// handoff.mjs — Typed handoffs between pipeline stages
|
|
2
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, unlinkSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
export const HANDOFF_TYPES = {
|
|
6
|
-
'think-to-work': { required: ['objective', 'files', 'criteria'], optional: ['context', 'confidence'] },
|
|
7
|
-
'work-to-review': { required: ['filesChanged', 'objective'], optional: ['diff', 'criteria', 'testsRun'] },
|
|
8
|
-
'review-to-head': { required: ['pass'], optional: ['findings', 'recommendation', 'severity'] },
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const hDir = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'handoffs');
|
|
12
|
-
const hPath = (id, f, t, cwd) => join(hDir(cwd), `${id}_${f}_${t}.json`);
|
|
13
|
-
|
|
14
|
-
function validate(from, to, data) {
|
|
15
|
-
const schema = HANDOFF_TYPES[`${from}-to-${to}`];
|
|
16
|
-
if (!schema) return;
|
|
17
|
-
for (const f of schema.required) {
|
|
18
|
-
if (!(f in data)) process.stderr.write(`[handoff] warn: missing required field '${f}' in ${from}-to-${to}\n`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createHandoff(fromStage, toStage, data, runId, cwd) {
|
|
23
|
-
try {
|
|
24
|
-
validate(fromStage, toStage, data);
|
|
25
|
-
const safeRunId = String(runId).replace(/[^a-z0-9_-]/gi, '_').slice(0, 50);
|
|
26
|
-
const dir = hDir(cwd);
|
|
27
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
28
|
-
const record = { fromStage, toStage, runId: safeRunId, createdAt: new Date().toISOString(), data };
|
|
29
|
-
const dest = hPath(safeRunId, fromStage, toStage, cwd); const tmp = dest + '.tmp';
|
|
30
|
-
writeFileSync(tmp, JSON.stringify(record, null, 2), 'utf8');
|
|
31
|
-
try { renameSync(tmp, dest); } catch { writeFileSync(dest, JSON.stringify(record, null, 2), 'utf8'); }
|
|
32
|
-
return record;
|
|
33
|
-
} catch { return null; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function consumeHandoff(runId, fromStage, toStage, cwd) {
|
|
37
|
-
try {
|
|
38
|
-
const safeRunId = String(runId).replace(/[^a-z0-9_-]/gi, '_').slice(0, 50);
|
|
39
|
-
const p = hPath(safeRunId, fromStage, toStage, cwd);
|
|
40
|
-
if (!existsSync(p)) return null;
|
|
41
|
-
const record = JSON.parse(readFileSync(p, 'utf8'));
|
|
42
|
-
try { unlinkSync(p); } catch { /* best-effort */ }
|
|
43
|
-
return record;
|
|
44
|
-
} catch { return null; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function buildHandoffContext(handoff, targetRole) {
|
|
48
|
-
if (!handoff?.data) return '';
|
|
49
|
-
const d = handoff.data;
|
|
50
|
-
const lines = (...parts) => parts.filter(Boolean).join('\n');
|
|
51
|
-
const list = (v) => Array.isArray(v) ? v.join(', ') : (v ?? '');
|
|
52
|
-
const items = (v) => Array.isArray(v) ? v.map(x => ` - ${x}`).join('\n') : (v ?? '');
|
|
53
|
-
|
|
54
|
-
if (targetRole === 'worker' && handoff.fromStage === 'thinker') return lines(
|
|
55
|
-
'## Handoff from Think Stage',
|
|
56
|
-
`**Objective:** ${d.objective ?? '(none)'}`,
|
|
57
|
-
`**Files in scope:** ${list(d.files) || 'unspecified'}`,
|
|
58
|
-
d.criteria ? `**Acceptance criteria:**\n${items(d.criteria)}` : '',
|
|
59
|
-
d.context ? `**Context:** ${d.context}` : '',
|
|
60
|
-
d.confidence != null ? `**Thinker confidence:** ${d.confidence}` : '',
|
|
61
|
-
);
|
|
62
|
-
if (targetRole === 'reviewer' && handoff.fromStage === 'worker') return lines(
|
|
63
|
-
'## Handoff from Work Stage',
|
|
64
|
-
`**Objective:** ${d.objective ?? '(none)'}`,
|
|
65
|
-
`**Files changed:** ${list(d.filesChanged) || 'unknown'}`,
|
|
66
|
-
d.criteria ? `**Original criteria:** ${Array.isArray(d.criteria) ? d.criteria.join('; ') : d.criteria}` : '',
|
|
67
|
-
d.testsRun ? `**Tests run:** ${d.testsRun}` : '',
|
|
68
|
-
d.diff ? `**Diff summary:**\n\`\`\`\n${d.diff.slice(0, 1200)}\n\`\`\`` : '',
|
|
69
|
-
);
|
|
70
|
-
if (targetRole === 'head' && handoff.fromStage === 'reviewer') return lines(
|
|
71
|
-
`## Review Result: ${d.pass ? 'PASS' : 'FAIL'}`,
|
|
72
|
-
d.findings ? `**Findings:**\n${items(d.findings)}` : '',
|
|
73
|
-
d.recommendation ? `**Recommendation:** ${d.recommendation}` : '',
|
|
74
|
-
d.severity ? `**Severity:** ${d.severity}` : '',
|
|
75
|
-
);
|
|
76
|
-
return JSON.stringify(handoff.data, null, 2);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function cleanupHandoffs(runId, cwd) {
|
|
80
|
-
try {
|
|
81
|
-
const dir = hDir(cwd);
|
|
82
|
-
if (!existsSync(dir)) return;
|
|
83
|
-
for (const name of readdirSync(dir)) {
|
|
84
|
-
if (name.startsWith(`${runId}_`)) try { unlinkSync(join(dir, name)); } catch { /* best-effort */ }
|
|
85
|
-
}
|
|
86
|
-
} catch { /* non-throwing */ }
|
|
87
|
-
}
|
package/src/head-protocol.mjs
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { processTurn, loadState } from './head.mjs';
|
|
4
|
-
|
|
5
|
-
const DUALBRAIN = join(process.cwd(), '.dualbrain');
|
|
6
|
-
const DELIBERATION_FILE = join(DUALBRAIN, 'deliberation.json');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Write the deliberation artifact after running HEAD's cognitive pipeline.
|
|
10
|
-
* This is the contract between HEAD's thinking and the deliberation-gate hook.
|
|
11
|
-
*
|
|
12
|
-
* @param {string} userMessage - The user's message to deliberate on
|
|
13
|
-
* @param {object} context - Optional context (files, priorFailures, patterns, etc.)
|
|
14
|
-
* @returns {object} The full deliberation result from processTurn
|
|
15
|
-
*/
|
|
16
|
-
export function writeDeliberation(userMessage, context = {}) {
|
|
17
|
-
const state = loadState();
|
|
18
|
-
const result = processTurn(state, userMessage, context);
|
|
19
|
-
|
|
20
|
-
// Determine if there are multiple independent sub-tasks that could be parallelized
|
|
21
|
-
const dispatchPlan = _deriveDispatchPlan(result, context);
|
|
22
|
-
|
|
23
|
-
// Build the artifact
|
|
24
|
-
const artifact = {
|
|
25
|
-
timestamp: Date.now(),
|
|
26
|
-
createdAt: Date.now(),
|
|
27
|
-
message: userMessage.slice(0, 500),
|
|
28
|
-
|
|
29
|
-
// Core deliberation fields
|
|
30
|
-
action: result.action,
|
|
31
|
-
result: result.result,
|
|
32
|
-
shouldAskUser: result.shouldAskUser,
|
|
33
|
-
confidence: result.result?.confidence || null,
|
|
34
|
-
|
|
35
|
-
// Obligations and noticings
|
|
36
|
-
obligations: result.obligations || [],
|
|
37
|
-
surfaceNoticings: result.result?.surfaceNoticings || [],
|
|
38
|
-
|
|
39
|
-
// Dispatch plan (parallel-wave support)
|
|
40
|
-
dispatchPlan,
|
|
41
|
-
|
|
42
|
-
// Metadata
|
|
43
|
-
depth: result.depth,
|
|
44
|
-
rationale: result.rationale,
|
|
45
|
-
situation: {
|
|
46
|
-
taskShape: result.situation?.taskShape || null,
|
|
47
|
-
urgency: result.situation?.urgency || null,
|
|
48
|
-
scope: result.situation?.taskShape?.scope || null,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Write atomically
|
|
53
|
-
mkdirSync(DUALBRAIN, { recursive: true });
|
|
54
|
-
const tmpFile = DELIBERATION_FILE + '.tmp.' + process.pid;
|
|
55
|
-
writeFileSync(tmpFile, JSON.stringify(artifact, null, 2));
|
|
56
|
-
renameSync(tmpFile, DELIBERATION_FILE);
|
|
57
|
-
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Read the current deliberation artifact.
|
|
63
|
-
* @returns {object|null} The deliberation artifact, or null if not found/unreadable.
|
|
64
|
-
*/
|
|
65
|
-
export function readDeliberation() {
|
|
66
|
-
try {
|
|
67
|
-
if (!existsSync(DELIBERATION_FILE)) return null;
|
|
68
|
-
return JSON.parse(readFileSync(DELIBERATION_FILE, 'utf8'));
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Check if the current deliberation is fresh (within maxAgeMs).
|
|
76
|
-
* @param {number} maxAgeMs - Maximum age in milliseconds (default 60000)
|
|
77
|
-
* @returns {boolean} True if deliberation exists and is fresh.
|
|
78
|
-
*/
|
|
79
|
-
export function isDeliberationFresh(maxAgeMs = 60_000) {
|
|
80
|
-
const delib = readDeliberation();
|
|
81
|
-
if (!delib) return false;
|
|
82
|
-
const timestamp = delib.timestamp || delib.createdAt || 0;
|
|
83
|
-
return (Date.now() - timestamp) <= maxAgeMs;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Derive a dispatch plan when the situation has multiple independent sub-tasks.
|
|
90
|
-
* Returns null if parallel dispatch is not applicable.
|
|
91
|
-
*/
|
|
92
|
-
function _deriveDispatchPlan(result, context) {
|
|
93
|
-
const situation = result.situation;
|
|
94
|
-
if (!situation) return null;
|
|
95
|
-
|
|
96
|
-
// Only generate a parallel plan for multi-file, non-trivial work
|
|
97
|
-
const scope = situation.taskShape?.scope;
|
|
98
|
-
const actionType = result.action?.type;
|
|
99
|
-
|
|
100
|
-
if (scope !== 'large' && scope !== 'medium') return null;
|
|
101
|
-
if (actionType !== 'dispatch' && actionType !== 'proceed') return null;
|
|
102
|
-
|
|
103
|
-
// Check for independent sub-tasks from context
|
|
104
|
-
const subtasks = context.subtasks || [];
|
|
105
|
-
if (subtasks.length < 2) {
|
|
106
|
-
// Heuristic: large scope with multiple files could be parallel
|
|
107
|
-
const fileCount = situation.material?.touchedFiles?.length || 0;
|
|
108
|
-
if (fileCount < 3) return null;
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
strategy: 'parallel-wave',
|
|
112
|
-
id: Date.now().toString(36),
|
|
113
|
-
expectedParallel: Math.min(fileCount, 5),
|
|
114
|
-
waveSize: Math.min(fileCount, 5),
|
|
115
|
-
reason: `${fileCount} independent files detected`,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
strategy: 'parallel-wave',
|
|
121
|
-
id: Date.now().toString(36),
|
|
122
|
-
expectedParallel: subtasks.length,
|
|
123
|
-
waveSize: subtasks.length,
|
|
124
|
-
subtasks: subtasks.map(t => typeof t === 'string' ? t : t.name || t.description),
|
|
125
|
-
reason: `${subtasks.length} independent sub-tasks identified`,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|