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
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pipeline.ts — Unified Pipeline for dual-brain.
|
|
3
|
+
// Every feature (go, think, review, watch, auto-commit, pr-triage, wave) routes through here.
|
|
4
|
+
// Exports: runPipeline, buildExecutionPlan, formatExecutionPlan, createPipelineRun
|
|
5
|
+
// Gate exports: contextGate, planningGate, principleGate, executionGate, outcomeGate
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { detectTask } from './detect.js';
|
|
9
|
+
import { decideRoute, getWorkStyle, WORK_STYLES } from './decide.js';
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
import { dispatch } from './dispatch.js';
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
import { loadProfile } from './profile.js';
|
|
14
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
import { buildContextPack as buildContextPackIntel } from './context.js';
|
|
18
|
+
import { compilePacket } from './context-intel.js';
|
|
19
|
+
// Lazy-load collaboration module
|
|
20
|
+
let _collab = null;
|
|
21
|
+
async function getCollab() {
|
|
22
|
+
if (!_collab) {
|
|
23
|
+
try {
|
|
24
|
+
_collab = await import('./collaboration.js');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
_collab = false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return _collab || null;
|
|
31
|
+
}
|
|
32
|
+
// ─── PipelineRun factory ──────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Create a fresh PipelineRun object.
|
|
35
|
+
*/
|
|
36
|
+
export function createPipelineRun(trigger = '', prompt = '') {
|
|
37
|
+
return {
|
|
38
|
+
id: randomUUID(),
|
|
39
|
+
startedAt: Date.now(),
|
|
40
|
+
trigger,
|
|
41
|
+
prompt,
|
|
42
|
+
// Phase 0: Intelligence
|
|
43
|
+
projectBrief: null,
|
|
44
|
+
taskBrief: null,
|
|
45
|
+
contradictions: [],
|
|
46
|
+
situationBrief: null,
|
|
47
|
+
// Phase 1: Context
|
|
48
|
+
context: null,
|
|
49
|
+
failureHistory: null,
|
|
50
|
+
priorOutcomes: null,
|
|
51
|
+
// Gate results
|
|
52
|
+
gates: {
|
|
53
|
+
context: null,
|
|
54
|
+
planning: null,
|
|
55
|
+
principle: null,
|
|
56
|
+
execution: null,
|
|
57
|
+
outcome: null,
|
|
58
|
+
},
|
|
59
|
+
// Phase 2: Plan
|
|
60
|
+
plan: null,
|
|
61
|
+
// Phase 3: Execution
|
|
62
|
+
result: null,
|
|
63
|
+
// Phase 4: Verification
|
|
64
|
+
verification: null,
|
|
65
|
+
// Phase 5: Outcome
|
|
66
|
+
outcome: null,
|
|
67
|
+
// Ledger + calibration
|
|
68
|
+
taskId: null,
|
|
69
|
+
openTasks: [],
|
|
70
|
+
calibration: null,
|
|
71
|
+
adaptation: null,
|
|
72
|
+
// Prompt intelligence + environment
|
|
73
|
+
promptAnalysis: null,
|
|
74
|
+
enrichedPrompt: null,
|
|
75
|
+
environment: null,
|
|
76
|
+
modelSuggestion: null,
|
|
77
|
+
// Think-engine fields
|
|
78
|
+
thinkResult: null,
|
|
79
|
+
decisionPreflight: null,
|
|
80
|
+
// Session history context
|
|
81
|
+
sessionContext: null,
|
|
82
|
+
// Replit context
|
|
83
|
+
replitEnvironment: null,
|
|
84
|
+
replitTools: null,
|
|
85
|
+
replitConfig: null,
|
|
86
|
+
// Execution safety
|
|
87
|
+
checkpoint: null,
|
|
88
|
+
// HEAD cognitive judgment
|
|
89
|
+
headJudgment: null,
|
|
90
|
+
// Collaboration
|
|
91
|
+
collaboration: null,
|
|
92
|
+
completedAt: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ─── Gate helpers ─────────────────────────────────────────────────────────────
|
|
96
|
+
function gate(passed, reason) {
|
|
97
|
+
return { passed: Boolean(passed), reason: reason ?? '' };
|
|
98
|
+
}
|
|
99
|
+
// ─── Principle predicates ─────────────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Block if 2 or more prior failures on the same approach.
|
|
102
|
+
*/
|
|
103
|
+
function rejectsRepeatedFailedApproach(run) {
|
|
104
|
+
const count = run.failureHistory?.failureCount ?? 0;
|
|
105
|
+
if (count >= 2) {
|
|
106
|
+
return { blocked: true, reason: `${count} prior failures on similar approach — must change strategy or use dual-brain` };
|
|
107
|
+
}
|
|
108
|
+
return { blocked: false };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Block if no plan is present.
|
|
112
|
+
*/
|
|
113
|
+
function requiresApprovedPlan(run) {
|
|
114
|
+
if (!run.plan) {
|
|
115
|
+
return { blocked: true, reason: 'No execution plan — pipeline cannot proceed without a plan' };
|
|
116
|
+
}
|
|
117
|
+
return { blocked: false };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Warn if plan touches more than 10 files or 3+ unrelated areas.
|
|
121
|
+
* Not a hard block — returns warning in reason but blocked: false.
|
|
122
|
+
*/
|
|
123
|
+
function rejectsScopeCreep(run) {
|
|
124
|
+
const fileCount = run.context?.files?.explicit?.length ?? 0;
|
|
125
|
+
const extractedCount = run.context?.files?.extracted?.length ?? 0;
|
|
126
|
+
const total = fileCount + extractedCount;
|
|
127
|
+
if (total > 10) {
|
|
128
|
+
return { blocked: false, reason: `Scope warning: plan touches ${total} files — consider splitting into smaller tasks` };
|
|
129
|
+
}
|
|
130
|
+
return { blocked: false };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Block high/critical risk tasks that have no challenger configured.
|
|
134
|
+
*/
|
|
135
|
+
function requiresDualBrainForHighRisk(run) {
|
|
136
|
+
const risk = run.context?.detection?.risk ?? 'low';
|
|
137
|
+
const hasChallenger = run.plan?.useChallenger && run.plan?.challengerModel;
|
|
138
|
+
if ((risk === 'high' || risk === 'critical') && !hasChallenger) {
|
|
139
|
+
return { blocked: true, reason: `High-risk task (${risk}) requires dual-brain challenger — configure OpenAI provider or lower risk scope` };
|
|
140
|
+
}
|
|
141
|
+
return { blocked: false };
|
|
142
|
+
}
|
|
143
|
+
// ─── Five mandatory gates ─────────────────────────────────────────────────────
|
|
144
|
+
/**
|
|
145
|
+
* Gate 1: Context gate.
|
|
146
|
+
* Passes only if failureHistory and priorOutcomes were actually queried (not null).
|
|
147
|
+
*/
|
|
148
|
+
export function contextGate(run) {
|
|
149
|
+
if (run.failureHistory === null) {
|
|
150
|
+
return gate(false, 'failureHistory was never queried — context phase incomplete');
|
|
151
|
+
}
|
|
152
|
+
if (run.priorOutcomes === null) {
|
|
153
|
+
return gate(false, 'priorOutcomes was never queried — context phase incomplete');
|
|
154
|
+
}
|
|
155
|
+
if (run.context === null) {
|
|
156
|
+
return gate(false, 'context pack was never built — context phase incomplete');
|
|
157
|
+
}
|
|
158
|
+
return gate(true, 'context loaded');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Gate 2: Planning gate.
|
|
162
|
+
* Passes if plan exists AND the proposed approach doesn't repeat a known failure.
|
|
163
|
+
*/
|
|
164
|
+
export function planningGate(run) {
|
|
165
|
+
if (!run.plan) {
|
|
166
|
+
return gate(false, 'No execution plan built');
|
|
167
|
+
}
|
|
168
|
+
// Check if the approach matches a prior failure
|
|
169
|
+
const history = run.failureHistory;
|
|
170
|
+
if (history?.hasPriorFailures && history?.escalation?.recommended) {
|
|
171
|
+
const esc = history.escalation;
|
|
172
|
+
// If the plan doesn't reflect the escalation (still using low depth when ultra is recommended)
|
|
173
|
+
const planDepth = run.plan.reasoningDepth ?? 'low';
|
|
174
|
+
const needsDepth = esc.toDepth ?? 'low';
|
|
175
|
+
const depthOrder = ['low', 'medium', 'high', 'ultra'];
|
|
176
|
+
const planIdx = depthOrder.indexOf(planDepth);
|
|
177
|
+
const needsIdx = depthOrder.indexOf(needsDepth);
|
|
178
|
+
if (planIdx < needsIdx) {
|
|
179
|
+
return gate(false, `Plan uses ${planDepth} reasoning but prior failures require ${needsDepth}. ${esc.reason}. Use a different strategy.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return gate(true, 'plan approved');
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Gate 3: Principle gate.
|
|
186
|
+
* Runs all principle predicates — any hard block fails the gate.
|
|
187
|
+
*/
|
|
188
|
+
export function principleGate(run) {
|
|
189
|
+
const checks = [
|
|
190
|
+
rejectsRepeatedFailedApproach(run),
|
|
191
|
+
requiresApprovedPlan(run),
|
|
192
|
+
rejectsScopeCreep(run),
|
|
193
|
+
requiresDualBrainForHighRisk(run),
|
|
194
|
+
];
|
|
195
|
+
const blocked = checks.find(c => c.blocked);
|
|
196
|
+
if (blocked) {
|
|
197
|
+
return gate(false, blocked.reason);
|
|
198
|
+
}
|
|
199
|
+
// Collect non-blocking warnings for the reason field
|
|
200
|
+
const warnings = checks.filter(c => !c.blocked && c.reason).map(c => c.reason);
|
|
201
|
+
return gate(true, warnings.length ? warnings.join('; ') : 'all principles satisfied');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Gate 4: Execution gate.
|
|
205
|
+
* Final "cleared to work?" check — all previous gates must have passed and plan must exist.
|
|
206
|
+
*/
|
|
207
|
+
export function executionGate(run) {
|
|
208
|
+
const prevGates = ['context', 'planning', 'principle'];
|
|
209
|
+
for (const name of prevGates) {
|
|
210
|
+
const g = run.gates[name];
|
|
211
|
+
if (!g || !g.passed) {
|
|
212
|
+
return gate(false, `Upstream gate '${name}' did not pass — cannot proceed to execution`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!run.plan) {
|
|
216
|
+
return gate(false, 'No plan present at execution gate');
|
|
217
|
+
}
|
|
218
|
+
return gate(true, 'cleared for execution');
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Gate 5: Outcome gate.
|
|
222
|
+
* After execution, checks that an outcome was recorded.
|
|
223
|
+
*/
|
|
224
|
+
export function outcomeGate(run) {
|
|
225
|
+
if (run.result && run.outcome === null) {
|
|
226
|
+
return gate(false, 'Execution completed but outcome was not recorded');
|
|
227
|
+
}
|
|
228
|
+
return gate(true, 'outcome recorded');
|
|
229
|
+
}
|
|
230
|
+
// ─── Context Pack ─────────────────────────────────────────────────────────────
|
|
231
|
+
/**
|
|
232
|
+
* Build a context pack from the raw inputs.
|
|
233
|
+
*/
|
|
234
|
+
async function buildContextPack(prompt, files = [], cwd = process.cwd(), sessionContext = null, headJudgment = null) {
|
|
235
|
+
const profile = await _loadProfileSafe(cwd);
|
|
236
|
+
const priorFailures = _getPriorFailures(prompt, cwd);
|
|
237
|
+
const detection = detectTask({
|
|
238
|
+
prompt,
|
|
239
|
+
files,
|
|
240
|
+
priorFailures,
|
|
241
|
+
sessionContext: sessionContext,
|
|
242
|
+
headJudgment: headJudgment,
|
|
243
|
+
});
|
|
244
|
+
const det = detection;
|
|
245
|
+
return {
|
|
246
|
+
prompt,
|
|
247
|
+
files: { explicit: files, extracted: det.specialist?.triggers ?? [] },
|
|
248
|
+
detection: det,
|
|
249
|
+
profile,
|
|
250
|
+
priorFailures,
|
|
251
|
+
cwd,
|
|
252
|
+
sessionContext,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// ─── Reasoning depth ──────────────────────────────────────────────────────────
|
|
256
|
+
const UNCERTAINTY_WORDS = /\b(not sure|maybe|should we|perhaps|architect|design|unsure|consider|what if|would it be|thinking about)\b/i;
|
|
257
|
+
/**
|
|
258
|
+
* Classify reasoning depth from context pack signals.
|
|
259
|
+
*/
|
|
260
|
+
export function classifyReasoningDepth(contextPack) {
|
|
261
|
+
const { detection, files, priorFailures = 0, prompt = '' } = contextPack;
|
|
262
|
+
const { risk = 'low', tier } = detection;
|
|
263
|
+
const fileCount = files.explicit.length;
|
|
264
|
+
if (risk === 'critical' ||
|
|
265
|
+
tier === 'think' ||
|
|
266
|
+
priorFailures >= 2 ||
|
|
267
|
+
UNCERTAINTY_WORDS.test(prompt))
|
|
268
|
+
return 'ultra';
|
|
269
|
+
if (risk === 'high' ||
|
|
270
|
+
fileCount > 5 ||
|
|
271
|
+
detection.complexity === 'complex')
|
|
272
|
+
return 'high';
|
|
273
|
+
if (risk === 'medium' ||
|
|
274
|
+
(fileCount >= 3 && fileCount <= 5) ||
|
|
275
|
+
detection.complexity === 'moderate')
|
|
276
|
+
return 'medium';
|
|
277
|
+
return 'low';
|
|
278
|
+
}
|
|
279
|
+
// ─── Challenger policy ────────────────────────────────────────────────────────
|
|
280
|
+
const THINK_TRIGGERS = new Set(['think', 'review']);
|
|
281
|
+
/**
|
|
282
|
+
* Determine whether challenger activates based on work style and risk.
|
|
283
|
+
*/
|
|
284
|
+
function shouldUseChallenger(contextPack, trigger) {
|
|
285
|
+
const { detection, profile, priorFailures = 0 } = contextPack;
|
|
286
|
+
const { risk = 'low' } = detection;
|
|
287
|
+
// Always challenger for think/review triggers with prior failures or design impact
|
|
288
|
+
if (priorFailures >= 2 || detection.designImpact || THINK_TRIGGERS.has(trigger))
|
|
289
|
+
return true;
|
|
290
|
+
const style = getWorkStyle(profile);
|
|
291
|
+
if (style.challengerPolicy === 'never')
|
|
292
|
+
return false;
|
|
293
|
+
if (style.challengerPolicy === 'high-risk')
|
|
294
|
+
return risk === 'high' || risk === 'critical';
|
|
295
|
+
if (style.challengerPolicy === 'medium-risk')
|
|
296
|
+
return risk !== 'low';
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Determine whether a checkpoint is required based on work style and risk.
|
|
301
|
+
*/
|
|
302
|
+
function shouldCreateCheckpoint(contextPack) {
|
|
303
|
+
const { detection, profile } = contextPack;
|
|
304
|
+
const { risk = 'low', tier = 'execute' } = detection;
|
|
305
|
+
const style = getWorkStyle(profile);
|
|
306
|
+
if (style.checkpointPolicy === 'never')
|
|
307
|
+
return false;
|
|
308
|
+
if (style.checkpointPolicy === 'all-edits')
|
|
309
|
+
return tier !== 'search';
|
|
310
|
+
if (style.checkpointPolicy === 'risky-ops')
|
|
311
|
+
return risk === 'high' || risk === 'critical';
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
// ─── Challenger model resolver ────────────────────────────────────────────────
|
|
315
|
+
function resolveChallenger(useChallenger, contextPack) {
|
|
316
|
+
if (!useChallenger)
|
|
317
|
+
return null;
|
|
318
|
+
const profile = contextPack.profile;
|
|
319
|
+
const providers = profile?.providers;
|
|
320
|
+
const openai = providers?.openai;
|
|
321
|
+
const openaiEnabled = openai?.enabled && openai?.plan;
|
|
322
|
+
if (!openaiEnabled)
|
|
323
|
+
return null;
|
|
324
|
+
const plan = openai.plan;
|
|
325
|
+
// Pick the best available OpenAI model for the challenger role
|
|
326
|
+
if (plan === '$100' || plan === '$200')
|
|
327
|
+
return 'o3'; // doctor:verified — config value comparison, not UI display
|
|
328
|
+
return 'gpt-4o';
|
|
329
|
+
}
|
|
330
|
+
// ─── Build execution plan ─────────────────────────────────────────────────────
|
|
331
|
+
/**
|
|
332
|
+
* Build an execution plan from context pack + trigger + options.
|
|
333
|
+
*/
|
|
334
|
+
export function buildExecutionPlan(contextPack, trigger, options = {}) {
|
|
335
|
+
const { detection, profile, priorFailures = 0 } = contextPack;
|
|
336
|
+
const reasoningDepth = options.forceDepth ?? classifyReasoningDepth(contextPack);
|
|
337
|
+
const useChallenger = options.forceChallenger || shouldUseChallenger(contextPack, trigger);
|
|
338
|
+
const challengerModel = resolveChallenger(useChallenger, contextPack);
|
|
339
|
+
const checkpointRequired = shouldCreateCheckpoint(contextPack);
|
|
340
|
+
// Work style for display and routing context
|
|
341
|
+
const workStyleObj = getWorkStyle(profile);
|
|
342
|
+
const workStyle = workStyleObj.key;
|
|
343
|
+
// Map reasoning depth → effort hint for decideRoute
|
|
344
|
+
const depthToEffort = { low: 'low', medium: 'medium', high: 'high', ultra: 'xhigh' };
|
|
345
|
+
const detectionWithDepth = {
|
|
346
|
+
...detection,
|
|
347
|
+
effort: depthToEffort[reasoningDepth] ?? detection.effort,
|
|
348
|
+
};
|
|
349
|
+
const decision = decideRoute({ profile: profile, detection: detectionWithDepth, cwd: contextPack.cwd, thinkResult: options.thinkResult, sessionContext: (contextPack.sessionContext ?? null) });
|
|
350
|
+
// Resolve full model ID for display (mirrors dispatch.mjs CLAUDE_MODEL_IDS)
|
|
351
|
+
const CLAUDE_MODEL_IDS = { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001' };
|
|
352
|
+
const modelAlias = decision.model ?? 'sonnet';
|
|
353
|
+
const displayModel = decision.provider === 'claude'
|
|
354
|
+
? (CLAUDE_MODEL_IDS[modelAlias] ?? modelAlias)
|
|
355
|
+
: modelAlias;
|
|
356
|
+
const verificationRequired = detection.tier !== 'search';
|
|
357
|
+
const approvalRequired = detection.risk === 'critical';
|
|
358
|
+
const explanation = _buildPlanExplanation({
|
|
359
|
+
displayModel,
|
|
360
|
+
reasoningDepth,
|
|
361
|
+
useChallenger,
|
|
362
|
+
workStyle,
|
|
363
|
+
workStyleObj,
|
|
364
|
+
decision,
|
|
365
|
+
detection,
|
|
366
|
+
priorFailures,
|
|
367
|
+
trigger,
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
primaryModel: displayModel,
|
|
371
|
+
primaryProvider: decision.provider ?? 'claude',
|
|
372
|
+
reasoningDepth,
|
|
373
|
+
useChallenger,
|
|
374
|
+
challengerModel,
|
|
375
|
+
workStyle,
|
|
376
|
+
checkpointRequired,
|
|
377
|
+
tier: detection.tier,
|
|
378
|
+
verificationRequired,
|
|
379
|
+
approvalRequired,
|
|
380
|
+
explanation,
|
|
381
|
+
_decision: decision,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function _buildPlanExplanation({ displayModel, reasoningDepth, useChallenger, workStyle, workStyleObj, decision, detection, priorFailures, trigger }) {
|
|
385
|
+
const parts = [];
|
|
386
|
+
const det = detection;
|
|
387
|
+
const modelShort = displayModel.split('/').pop();
|
|
388
|
+
parts.push(`${modelShort} for ${det.risk}-risk ${det.intent}`);
|
|
389
|
+
const styleLabel = workStyleObj?.label ?? workStyle ?? 'balanced';
|
|
390
|
+
parts.push(`style: ${styleLabel}`);
|
|
391
|
+
if (useChallenger) {
|
|
392
|
+
parts.push('challenger active');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
parts.push('no challenger needed');
|
|
396
|
+
}
|
|
397
|
+
if (priorFailures > 0) {
|
|
398
|
+
parts.push(`${priorFailures} prior failure${priorFailures > 1 ? 's' : ''}`);
|
|
399
|
+
}
|
|
400
|
+
return parts.join(', ');
|
|
401
|
+
}
|
|
402
|
+
// ─── Format execution plan ────────────────────────────────────────────────────
|
|
403
|
+
/**
|
|
404
|
+
* Return a human-readable display string for an execution plan.
|
|
405
|
+
*/
|
|
406
|
+
export function formatExecutionPlan(plan) {
|
|
407
|
+
const depthLabel = { low: 'low reasoning', medium: 'medium reasoning', high: 'high reasoning', ultra: 'ultra reasoning' };
|
|
408
|
+
// Work style label + challenger description
|
|
409
|
+
const styleKey = plan.workStyle ?? 'balanced';
|
|
410
|
+
const styleDef = { ...(WORK_STYLES[styleKey] ?? WORK_STYLES.balanced), key: styleKey };
|
|
411
|
+
const challengerNote = plan.useChallenger
|
|
412
|
+
? `challenger on${plan.challengerModel ? ` (${plan.challengerModel})` : ''}`
|
|
413
|
+
: `challenger off (policy: ${styleDef.challengerPolicy})`;
|
|
414
|
+
const lines = [
|
|
415
|
+
'⚡ Execution Plan',
|
|
416
|
+
` Model: ${plan.primaryModel} (${depthLabel[plan.reasoningDepth] ?? plan.reasoningDepth})`,
|
|
417
|
+
` Mode: ${styleDef.label} — ${challengerNote}`,
|
|
418
|
+
` Checkpoint: ${plan.checkpointRequired ? 'yes (risky operation detected)' : 'no'}`,
|
|
419
|
+
` Risk: ${plan._decision?.risk ?? 'unknown'} | Tier: ${plan.tier}`,
|
|
420
|
+
` Verify: ${plan.verificationRequired ? 'yes' : 'no'} | Approval: ${plan.approvalRequired ? 'yes' : 'no'}`,
|
|
421
|
+
` Why: ${plan.explanation}`,
|
|
422
|
+
];
|
|
423
|
+
return lines.join('\n');
|
|
424
|
+
}
|
|
425
|
+
// ─── Checkpoint ───────────────────────────────────────────────────────────────
|
|
426
|
+
/**
|
|
427
|
+
* Create a lightweight safety checkpoint before a risky operation.
|
|
428
|
+
* Tries git stash create first (non-destructive ref), falls back to recording HEAD.
|
|
429
|
+
* Always best-effort — never throws.
|
|
430
|
+
*/
|
|
431
|
+
async function createCheckpoint(cwd, contextPack) {
|
|
432
|
+
try {
|
|
433
|
+
const checkpointDir = join(cwd, '.dualbrain', 'checkpoints');
|
|
434
|
+
mkdirSync(checkpointDir, { recursive: true });
|
|
435
|
+
let ref = null;
|
|
436
|
+
// Try git stash create (creates a stash object without modifying working tree)
|
|
437
|
+
try {
|
|
438
|
+
const stashRef = execSync('git stash create', { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
439
|
+
.toString().trim();
|
|
440
|
+
if (stashRef)
|
|
441
|
+
ref = stashRef;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// git stash create failed or no changes — fall through
|
|
445
|
+
}
|
|
446
|
+
// Fallback: record current HEAD
|
|
447
|
+
if (!ref) {
|
|
448
|
+
try {
|
|
449
|
+
ref = execSync('git rev-parse HEAD', { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
450
|
+
.toString().trim();
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
ref = 'unknown';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
457
|
+
const entry = {
|
|
458
|
+
timestamp: new Date().toISOString(),
|
|
459
|
+
ref,
|
|
460
|
+
prompt: contextPack.prompt?.slice(0, 120),
|
|
461
|
+
risk: contextPack.detection?.risk,
|
|
462
|
+
tier: contextPack.detection?.tier,
|
|
463
|
+
};
|
|
464
|
+
writeFileSync(join(checkpointDir, `${ts}.json`), JSON.stringify(entry, null, 2));
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Checkpoint is best-effort — never block execution
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// ─── Verification ─────────────────────────────────────────────────────────────
|
|
471
|
+
/**
|
|
472
|
+
* Verify the dispatch result meets basic expectations.
|
|
473
|
+
*/
|
|
474
|
+
async function verify(result, plan, cwd) {
|
|
475
|
+
const notes = [];
|
|
476
|
+
const r = result;
|
|
477
|
+
if (!r || r.status === 'error' || r.status === 'failed') {
|
|
478
|
+
return { ok: false, notes: ['Dispatch returned failure status'] };
|
|
479
|
+
}
|
|
480
|
+
if (plan.tier !== 'search') {
|
|
481
|
+
try {
|
|
482
|
+
const gitOut = execSync('git status --porcelain', { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
483
|
+
if (gitOut.trim()) {
|
|
484
|
+
notes.push(`Files changed (git status shows ${gitOut.trim().split('\n').length} modified)`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
notes.push('No file changes detected by git — verify task actually ran');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// git not available or not a repo — skip
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { ok: true, notes };
|
|
495
|
+
}
|
|
496
|
+
// ─── Outcome recording ────────────────────────────────────────────────────────
|
|
497
|
+
async function recordOutcomeSafe(run) {
|
|
498
|
+
try {
|
|
499
|
+
const { recordOutcome } = await import('./outcome.js');
|
|
500
|
+
const cwd = run.context?.cwd ?? process.cwd();
|
|
501
|
+
const recorded = await recordOutcome(run.plan, run.result, run.verification, cwd);
|
|
502
|
+
run.outcome = recorded;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// outcome module unavailable — silently skip
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// ─── Prior failures ───────────────────────────────────────────────────────────
|
|
509
|
+
// In-process cache of prior failures keyed by a rough prompt fingerprint.
|
|
510
|
+
// Populated by recordOutcomeSafe when outcome module is available; otherwise 0.
|
|
511
|
+
const _priorFailureCache = new Map();
|
|
512
|
+
function _getPriorFailures(prompt, _cwd) {
|
|
513
|
+
const key = prompt.slice(0, 40).toLowerCase().replace(/\s+/g, ' ');
|
|
514
|
+
return _priorFailureCache.get(key) ?? 0;
|
|
515
|
+
}
|
|
516
|
+
function _incrementFailureCache(prompt) {
|
|
517
|
+
const key = prompt.slice(0, 40).toLowerCase().replace(/\s+/g, ' ');
|
|
518
|
+
_priorFailureCache.set(key, (_priorFailureCache.get(key) ?? 0) + 1);
|
|
519
|
+
}
|
|
520
|
+
// ─── Profile loader (safe) ────────────────────────────────────────────────────
|
|
521
|
+
async function _loadProfileSafe(cwd) {
|
|
522
|
+
try {
|
|
523
|
+
return await loadProfile(cwd);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return {};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ─── Gate runner ─────────────────────────────────────────────────────────────
|
|
530
|
+
/**
|
|
531
|
+
* Run a named gate, store its result in run.gates, and return whether it passed.
|
|
532
|
+
* If gate throws, it is treated as a failure (fail-closed).
|
|
533
|
+
*/
|
|
534
|
+
function runGate(run, gateName, gateFn) {
|
|
535
|
+
let result;
|
|
536
|
+
try {
|
|
537
|
+
result = gateFn(run);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
541
|
+
result = gate(false, `Gate '${gateName}' threw: ${message}`);
|
|
542
|
+
}
|
|
543
|
+
// Treat missing result or missing passed field as fail-closed
|
|
544
|
+
if (!result || typeof result.passed !== 'boolean') {
|
|
545
|
+
result = gate(false, `Gate '${gateName}' returned invalid result`);
|
|
546
|
+
}
|
|
547
|
+
run.gates[gateName] = result;
|
|
548
|
+
return result.passed;
|
|
549
|
+
}
|
|
550
|
+
// ─── Pre-dispatch think (Position 1: context intelligence) ───────────────────
|
|
551
|
+
/**
|
|
552
|
+
* Optionally spawn a cheap think agent to produce a refined work spec before
|
|
553
|
+
* the real dispatch. Non-blocking on any failure.
|
|
554
|
+
*/
|
|
555
|
+
async function preDispatchThink(prompt, files, decision, cwd, profile, opts = {}) {
|
|
556
|
+
const log = opts.log ?? (() => { });
|
|
557
|
+
// Guard: never recurse
|
|
558
|
+
if (opts._skipPreDispatchThink) {
|
|
559
|
+
log('[dual-brain] pre-dispatch think: skipped (recursive call)');
|
|
560
|
+
return { refined: false };
|
|
561
|
+
}
|
|
562
|
+
// Guard: only execute/think tiers
|
|
563
|
+
const tier = decision?.tier ?? 'execute';
|
|
564
|
+
if (tier === 'search') {
|
|
565
|
+
log('[dual-brain] pre-dispatch think: skipped (search tier)');
|
|
566
|
+
return { refined: false };
|
|
567
|
+
}
|
|
568
|
+
// Guard: governance tier >= 2 (map tier names to numeric levels)
|
|
569
|
+
const TIER_LEVEL = { search: 1, execute: 2, think: 3 };
|
|
570
|
+
const tierLevel = TIER_LEVEL[tier] ?? 2;
|
|
571
|
+
if (tierLevel < 2) {
|
|
572
|
+
log('[dual-brain] pre-dispatch think: skipped (tier < 2)');
|
|
573
|
+
return { refined: false };
|
|
574
|
+
}
|
|
575
|
+
// Guard: decision confidence must be < 0.9
|
|
576
|
+
const confidence = decision?.confidence ?? 0.5;
|
|
577
|
+
if (confidence >= 0.9) {
|
|
578
|
+
log('[dual-brain] pre-dispatch think: skipped (confidence >= 0.9)');
|
|
579
|
+
return { refined: false };
|
|
580
|
+
}
|
|
581
|
+
// Guard: not cost-saver work style
|
|
582
|
+
try {
|
|
583
|
+
const style = getWorkStyle(profile);
|
|
584
|
+
if (style.key === 'cost-saver') {
|
|
585
|
+
log('[dual-brain] pre-dispatch think: skipped (cost-saver profile)');
|
|
586
|
+
return { refined: false };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// profile unavailable — proceed
|
|
591
|
+
}
|
|
592
|
+
// Auto-disable if ROI is bad (< 30% hit rate after 10+ observations)
|
|
593
|
+
{
|
|
594
|
+
const metricsPath = join(cwd, '.dualbrain', 'think-metrics.json');
|
|
595
|
+
let metrics = { hits: 0, misses: 0, totalTokens: 0 };
|
|
596
|
+
try {
|
|
597
|
+
metrics = JSON.parse(readFileSync(metricsPath, 'utf8'));
|
|
598
|
+
}
|
|
599
|
+
catch { }
|
|
600
|
+
if (metrics.hits + metrics.misses >= 10 && metrics.hits / (metrics.hits + metrics.misses) < 0.3) {
|
|
601
|
+
const verbose = opts.verbose ?? false;
|
|
602
|
+
if (verbose)
|
|
603
|
+
process.stderr.write('[dual-brain] pre-dispatch think disabled: hit rate below 30%\n');
|
|
604
|
+
return { refined: false, reason: 'think ROI too low, auto-disabled' };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
log('[dual-brain] pre-dispatch think: refining work spec...');
|
|
609
|
+
// Build the thinker context pack
|
|
610
|
+
const pack = await buildContextPackIntel(prompt, files, cwd);
|
|
611
|
+
// Compile to a thinker-shaped prompt (sonnet, 3000 token budget)
|
|
612
|
+
const thinkerPrompt = compilePacket(pack, 'thinker', 'sonnet', 3000);
|
|
613
|
+
// Dispatch to a think agent — use sonnet, tier=think, skip all extras
|
|
614
|
+
const thinkDecision = {
|
|
615
|
+
provider: 'claude',
|
|
616
|
+
model: 'sonnet',
|
|
617
|
+
tier: 'think',
|
|
618
|
+
confidence: 1, // internal call — fully confident
|
|
619
|
+
};
|
|
620
|
+
const thinkResult = await dispatch({
|
|
621
|
+
decision: thinkDecision,
|
|
622
|
+
prompt: thinkerPrompt,
|
|
623
|
+
files: [],
|
|
624
|
+
cwd,
|
|
625
|
+
dryRun: false,
|
|
626
|
+
verbose: false,
|
|
627
|
+
profile,
|
|
628
|
+
_skipPreDispatchThink: true,
|
|
629
|
+
_skipRelatedContext: true,
|
|
630
|
+
});
|
|
631
|
+
// Parse the think result — expect JSON with { decision, confidence, workSpec }
|
|
632
|
+
let parsed = null;
|
|
633
|
+
try {
|
|
634
|
+
const thinkObj = thinkResult;
|
|
635
|
+
const raw = typeof thinkResult === 'string'
|
|
636
|
+
? thinkResult
|
|
637
|
+
: (thinkObj?.output ?? thinkObj?.result ?? thinkObj?.text ?? JSON.stringify(thinkResult));
|
|
638
|
+
// Extract JSON from possible prose wrapping
|
|
639
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
640
|
+
if (jsonMatch) {
|
|
641
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// JSON parse failed — proceed unchanged
|
|
646
|
+
}
|
|
647
|
+
if (!parsed || typeof parsed.confidence !== 'number' || parsed.confidence <= 0.7) {
|
|
648
|
+
const reason = !parsed ? 'unparseable response' : `confidence ${parsed.confidence} <= 0.7`;
|
|
649
|
+
log(`[dual-brain] pre-dispatch think: skipped (${reason})`);
|
|
650
|
+
_recordThinkMetrics(false, cwd);
|
|
651
|
+
return { refined: false };
|
|
652
|
+
}
|
|
653
|
+
const ws = parsed.workSpec;
|
|
654
|
+
if (!ws || !ws.objective) {
|
|
655
|
+
log('[dual-brain] pre-dispatch think: skipped (no workSpec.objective)');
|
|
656
|
+
_recordThinkMetrics(false, cwd);
|
|
657
|
+
return { refined: false };
|
|
658
|
+
}
|
|
659
|
+
// Apply refinements
|
|
660
|
+
const newObjective = ws.objective;
|
|
661
|
+
const newFiles = [...new Set([...files, ...(ws.files ?? [])])];
|
|
662
|
+
const newDecision = ws.criteria?.length
|
|
663
|
+
? { ...decision, acceptanceCriteria: [...(decision.acceptanceCriteria ?? []), ...ws.criteria] }
|
|
664
|
+
: decision;
|
|
665
|
+
log(`[dual-brain] think refined: "${newObjective.slice(0, 60)}..." (confidence: ${parsed.confidence})`);
|
|
666
|
+
_recordThinkMetrics(true, cwd);
|
|
667
|
+
return {
|
|
668
|
+
refined: true,
|
|
669
|
+
prompt: newObjective,
|
|
670
|
+
files: newFiles,
|
|
671
|
+
decision: newDecision,
|
|
672
|
+
confidence: parsed.confidence,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
// Non-blocking on any failure
|
|
677
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
678
|
+
log(`[dual-brain] pre-dispatch think: skipped (error: ${message})`);
|
|
679
|
+
_recordThinkMetrics(false, cwd);
|
|
680
|
+
return { refined: false };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Record a think hit or miss into think-metrics.json (non-blocking).
|
|
685
|
+
*/
|
|
686
|
+
function _recordThinkMetrics(hit, cwd) {
|
|
687
|
+
try {
|
|
688
|
+
const metricsPath = join(cwd, '.dualbrain', 'think-metrics.json');
|
|
689
|
+
let metrics = { hits: 0, misses: 0, totalTokens: 0 };
|
|
690
|
+
try {
|
|
691
|
+
metrics = JSON.parse(readFileSync(metricsPath, 'utf8'));
|
|
692
|
+
}
|
|
693
|
+
catch { }
|
|
694
|
+
if (hit) {
|
|
695
|
+
metrics.hits++;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
metrics.misses++;
|
|
699
|
+
}
|
|
700
|
+
metrics.totalTokens += 3000; // budget per think call
|
|
701
|
+
metrics.lastUpdated = new Date().toISOString();
|
|
702
|
+
mkdirSync(join(cwd, '.dualbrain'), { recursive: true });
|
|
703
|
+
writeFileSync(metricsPath, JSON.stringify(metrics, null, 2) + '\n');
|
|
704
|
+
}
|
|
705
|
+
catch { /* non-blocking */ }
|
|
706
|
+
}
|
|
707
|
+
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
708
|
+
/**
|
|
709
|
+
* Run the unified pipeline.
|
|
710
|
+
*/
|
|
711
|
+
export async function runPipeline(trigger, prompt, options = {}) {
|
|
712
|
+
const { files = [], cwd = process.cwd(), dryRun = false, verbose = false, forceDepth, forceChallenger = false, silent = false, } = options;
|
|
713
|
+
const log = silent ? () => { } : (msg) => process.stderr.write(msg + '\n');
|
|
714
|
+
// Create the PipelineRun state object
|
|
715
|
+
const run = createPipelineRun(trigger, prompt);
|
|
716
|
+
try {
|
|
717
|
+
// ── Phase 0: HEAD Cognitive Judgment ─────────────────────────────────────
|
|
718
|
+
try {
|
|
719
|
+
const head = await import('./head.js');
|
|
720
|
+
const headState = head.loadState();
|
|
721
|
+
const headContext = {
|
|
722
|
+
files: files,
|
|
723
|
+
priorFailures: 0,
|
|
724
|
+
uncommittedFiles: [],
|
|
725
|
+
recentFiles: [],
|
|
726
|
+
patterns: [],
|
|
727
|
+
};
|
|
728
|
+
// Enrich head context from git state (best-effort)
|
|
729
|
+
try {
|
|
730
|
+
const gitStatus = execSync('git status --porcelain -u', { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
731
|
+
headContext.uncommittedFiles = gitStatus.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
732
|
+
}
|
|
733
|
+
catch { }
|
|
734
|
+
run.headJudgment = head.processTurn(headState, prompt, headContext);
|
|
735
|
+
// HEAD says to ask the user — block pipeline with the uncertainty + noticings
|
|
736
|
+
const hj = run.headJudgment;
|
|
737
|
+
const hjResult = hj?.result;
|
|
738
|
+
if (hj?.shouldAskUser && !options.forceDispatch) {
|
|
739
|
+
const reasons = [];
|
|
740
|
+
const confidence = hjResult?.confidence;
|
|
741
|
+
if (confidence?.level !== 'sufficient') {
|
|
742
|
+
reasons.push(`Confidence: ${confidence?.level} (${confidence?.score})`);
|
|
743
|
+
for (const gap of confidence?.gaps || []) {
|
|
744
|
+
reasons.push(` Uncertain: ${gap}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const n of hjResult?.surfaceNoticings || []) {
|
|
748
|
+
reasons.push(` ${n.type}: ${n.observation}`);
|
|
749
|
+
}
|
|
750
|
+
const action = hjResult?.action;
|
|
751
|
+
if (action?.type === 'clarify') {
|
|
752
|
+
reasons.push(`HEAD recommends clarifying before acting`);
|
|
753
|
+
}
|
|
754
|
+
run.completedAt = Date.now();
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
gateFailure: 'head-judgment',
|
|
758
|
+
reason: reasons.join('\n'),
|
|
759
|
+
headJudgment: run.headJudgment,
|
|
760
|
+
run,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (verbose) {
|
|
764
|
+
log(`[pipeline] HEAD depth: ${hj.depth}, action: ${(hjResult?.action || {}).type}/${(hjResult?.action || {}).mode}`);
|
|
765
|
+
if ((hjResult?.surfaceNoticings || []).length > 0) {
|
|
766
|
+
for (const n of hjResult?.surfaceNoticings || []) {
|
|
767
|
+
log(`[pipeline] HEAD noticed: ${n.observation}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
// head.mjs unavailable — continue degraded (no cognitive layer)
|
|
774
|
+
}
|
|
775
|
+
// ── Phase 0: Situational awareness ───────────────────────────────────────
|
|
776
|
+
const headDepth = run.headJudgment?.depth || 'full';
|
|
777
|
+
const loadFull = headDepth === 'full' || headDepth === 'deep';
|
|
778
|
+
const loadLight = loadFull || headDepth === 'light';
|
|
779
|
+
// Session history — always load (lightweight, index-only)
|
|
780
|
+
try {
|
|
781
|
+
const session = await import('./session.js');
|
|
782
|
+
if (session.getRoutingContext) {
|
|
783
|
+
run.sessionContext = session.getRoutingContext(cwd, prompt);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch { } // non-blocking
|
|
787
|
+
// Intelligence module — skip for reflexive
|
|
788
|
+
if (loadLight) {
|
|
789
|
+
try {
|
|
790
|
+
const { deriveProjectState, deriveTaskContext, detectContradictions, formatBrief } = await import('./intelligence.js');
|
|
791
|
+
run.projectBrief = await deriveProjectState(options.cwd || process.cwd());
|
|
792
|
+
run.taskBrief = deriveTaskContext(prompt, (options.recentEvents || []));
|
|
793
|
+
run.situationBrief = formatBrief(run.projectBrief, run.taskBrief, run.sessionContext);
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
// intelligence module not available — continue without it (degraded)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Doctor, ledger, calibration, awareness, replit, think-engine, prompt-intel
|
|
800
|
+
if (loadLight) {
|
|
801
|
+
// Doctor: discover capabilities (cached per process)
|
|
802
|
+
try {
|
|
803
|
+
const { discover, verifyAll } = await import('./doctor.js');
|
|
804
|
+
const doctorCwd = options.cwd || process.cwd();
|
|
805
|
+
discover(doctorCwd);
|
|
806
|
+
verifyAll(doctorCwd);
|
|
807
|
+
}
|
|
808
|
+
catch { }
|
|
809
|
+
// Ledger: check open tasks + create task
|
|
810
|
+
try {
|
|
811
|
+
const { getOpenTasks, createTask, reconcile } = await import('./ledger.js');
|
|
812
|
+
const ledgerCwd = options.cwd || process.cwd();
|
|
813
|
+
run.openTasks = getOpenTasks(ledgerCwd);
|
|
814
|
+
reconcile(ledgerCwd);
|
|
815
|
+
const task = createTask({
|
|
816
|
+
intent: prompt,
|
|
817
|
+
owner: 'head',
|
|
818
|
+
priority: run.projectBrief?.recentFailures?.length ? 'high' : 'medium',
|
|
819
|
+
files: options.files || []
|
|
820
|
+
}, ledgerCwd);
|
|
821
|
+
run.taskId = task.id;
|
|
822
|
+
}
|
|
823
|
+
catch { }
|
|
824
|
+
if (run.openTasks.length > 0) {
|
|
825
|
+
const preview = run.openTasks.slice(0, 3).map((t) => t.intent).join(', ');
|
|
826
|
+
const pendingLine = `PENDING TASKS: ${run.openTasks.length} open (${preview})`;
|
|
827
|
+
run.situationBrief = run.situationBrief
|
|
828
|
+
? `${run.situationBrief}\n${pendingLine}`
|
|
829
|
+
: pendingLine;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Heavy intelligence modules — only for full/deep
|
|
833
|
+
if (loadFull) {
|
|
834
|
+
// Calibration
|
|
835
|
+
try {
|
|
836
|
+
const { analyzeInput, getAdaptation, detectCorrection, updateCalibration } = await import('./calibration.js');
|
|
837
|
+
const { getProjectState, updateProject } = await import('./living-docs.js');
|
|
838
|
+
const calCwd = options.cwd || process.cwd();
|
|
839
|
+
const projectState = getProjectState(calCwd);
|
|
840
|
+
const currentCal = projectState?.project;
|
|
841
|
+
const userCal = currentCal?.userCalibration || { specificity: 3, corrections: 3, autonomy: 3, interactions: 0 };
|
|
842
|
+
const isCorrection = detectCorrection(prompt);
|
|
843
|
+
run.calibration = updateCalibration(userCal, prompt, isCorrection);
|
|
844
|
+
run.adaptation = getAdaptation(run.calibration);
|
|
845
|
+
updateProject({ userCalibration: run.calibration }, calCwd);
|
|
846
|
+
}
|
|
847
|
+
catch { }
|
|
848
|
+
// Environment awareness
|
|
849
|
+
try {
|
|
850
|
+
const { scanEnvironment, getCapabilitySummary } = await import('./awareness.js');
|
|
851
|
+
run.environment = scanEnvironment(cwd);
|
|
852
|
+
if (run.situationBrief && run.environment) {
|
|
853
|
+
const caps = getCapabilitySummary(run.environment);
|
|
854
|
+
if (caps.length > 0) {
|
|
855
|
+
run.situationBrief += '\nCAPABILITIES: ' + caps.join(', ');
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
catch { }
|
|
860
|
+
// Replit context
|
|
861
|
+
try {
|
|
862
|
+
const replit = await import('./replit.js');
|
|
863
|
+
const replitEnv = replit.detectReplitEnvironment(cwd);
|
|
864
|
+
if (replitEnv.isReplit) {
|
|
865
|
+
run.replitEnvironment = replitEnv;
|
|
866
|
+
run.replitTools = replit.inspectReplitTools(cwd);
|
|
867
|
+
run.replitConfig = replit.getReplitToolsConfig(cwd);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch { }
|
|
871
|
+
// Knowledge preflight
|
|
872
|
+
try {
|
|
873
|
+
const { lookupDecision, triageQuestion } = await import('./think-engine.js');
|
|
874
|
+
const teCwd = options.cwd || process.cwd();
|
|
875
|
+
run.decisionPreflight = lookupDecision(prompt, options.tags || [], teCwd);
|
|
876
|
+
const preflight = run.decisionPreflight;
|
|
877
|
+
if (preflight.recommendation === 'reuse' && preflight.candidates?.[0]) {
|
|
878
|
+
if (run.situationBrief) {
|
|
879
|
+
run.situationBrief += '\nCACHED DECISION: Found prior decision with ' +
|
|
880
|
+
Math.round(preflight.candidates[0].relevance * 100) + '% relevance';
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const triage = triageQuestion(prompt, run.projectBrief, run.decisionPreflight);
|
|
884
|
+
run.thinkResult = { tier: triage.recommendedTier, estimatedTokens: triage.estimatedTokens, triage };
|
|
885
|
+
if (run.situationBrief) {
|
|
886
|
+
run.situationBrief += '\nTHINK TIER: ' + triage.recommendedTier + ' (' + triage.estimatedTokens + ' tokens est.)';
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
catch { }
|
|
890
|
+
// Prompt intelligence
|
|
891
|
+
try {
|
|
892
|
+
const { analyzePrompt, enrichPrompt, shouldBlock, getBlockReason } = await import('./prompt-intel.js');
|
|
893
|
+
run.promptAnalysis = analyzePrompt(prompt, run.projectBrief, run.calibration);
|
|
894
|
+
if (shouldBlock(run.promptAnalysis)) {
|
|
895
|
+
const reason = getBlockReason(run.promptAnalysis) ?? '';
|
|
896
|
+
if (run.taskId) {
|
|
897
|
+
try {
|
|
898
|
+
const { failTask } = await import('./ledger.js');
|
|
899
|
+
failTask(run.taskId, 'Blocked by risk detection: ' + reason, cwd);
|
|
900
|
+
}
|
|
901
|
+
catch { }
|
|
902
|
+
}
|
|
903
|
+
run.completedAt = Date.now();
|
|
904
|
+
return {
|
|
905
|
+
success: false,
|
|
906
|
+
gateFailure: 'risk',
|
|
907
|
+
reason: 'Prompt blocked: ' + reason,
|
|
908
|
+
promptAnalysis: run.promptAnalysis,
|
|
909
|
+
run
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
const analysis = run.promptAnalysis;
|
|
913
|
+
if (analysis.intervention === 'silent_enrich' || analysis.intervention === 'confirm_rewrite') {
|
|
914
|
+
run.enrichedPrompt = enrichPrompt(prompt, run.projectBrief, run.promptAnalysis);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
catch { }
|
|
918
|
+
}
|
|
919
|
+
// ── Phase 1: Context ──────────────────────────────────────────────────────
|
|
920
|
+
const effectivePrompt = run.enrichedPrompt || prompt;
|
|
921
|
+
// Build context pack (pass sessionContext so detect can use cross-session signals)
|
|
922
|
+
run.context = await buildContextPack(effectivePrompt, files, cwd, run.sessionContext, run.headJudgment);
|
|
923
|
+
// Query failure history (must happen before context gate)
|
|
924
|
+
try {
|
|
925
|
+
const { checkFailureHistory } = await import('./failure-memory.js');
|
|
926
|
+
run.failureHistory = await checkFailureHistory(effectivePrompt, files, cwd);
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
// failure-memory.mjs unavailable — set to empty result so gate still passes
|
|
930
|
+
run.failureHistory = { hasPriorFailures: false, failureCount: 0, lastFailure: null, escalation: { recommended: false } };
|
|
931
|
+
}
|
|
932
|
+
// Query relevant outcomes (must happen before context gate)
|
|
933
|
+
try {
|
|
934
|
+
const { getRelevantOutcomes } = await import('./outcome.js');
|
|
935
|
+
run.priorOutcomes = await getRelevantOutcomes(effectivePrompt, files, cwd);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
// outcome module unavailable — set to empty array so gate still passes
|
|
939
|
+
run.priorOutcomes = [];
|
|
940
|
+
}
|
|
941
|
+
// Gate 1: Context gate
|
|
942
|
+
if (!runGate(run, 'context', contextGate)) {
|
|
943
|
+
run.completedAt = Date.now();
|
|
944
|
+
try {
|
|
945
|
+
const { recordEvent } = await import('./doctor.js');
|
|
946
|
+
recordEvent({ type: 'gate_failure', checkId: 'context-gate', severity: 'fail', outcome: 'blocked', evidence: run.gates.context.reason, sessionId: run.id }, cwd);
|
|
947
|
+
}
|
|
948
|
+
catch { /* non-blocking */ }
|
|
949
|
+
return { success: false, gateFailure: 'context', reason: run.gates.context.reason, run };
|
|
950
|
+
}
|
|
951
|
+
// ── Phase 2: Plan ─────────────────────────────────────────────────────────
|
|
952
|
+
// HEAD's depth assessment can influence the plan's reasoning depth
|
|
953
|
+
const headDepthMap = { reflexive: 'low', light: 'medium', full: 'high', deep: 'ultra' };
|
|
954
|
+
const headSuggestedDepth = run.headJudgment?.depth
|
|
955
|
+
? headDepthMap[run.headJudgment.depth]
|
|
956
|
+
: undefined;
|
|
957
|
+
const effectiveForceDepth = forceDepth || headSuggestedDepth;
|
|
958
|
+
run.plan = buildExecutionPlan(run.context, trigger, { forceDepth: effectiveForceDepth, forceChallenger, thinkResult: run.thinkResult });
|
|
959
|
+
// Model intelligence
|
|
960
|
+
try {
|
|
961
|
+
const { suggestModel, getRegistryAge } = await import('./models.js');
|
|
962
|
+
const availableProviders = [];
|
|
963
|
+
const env = run.environment;
|
|
964
|
+
const claudeCode = env?.claudeCode || {};
|
|
965
|
+
const tools = env?.tools || {};
|
|
966
|
+
if (claudeCode.isInsideClaude || tools.claude?.available)
|
|
967
|
+
availableProviders.push('anthropic');
|
|
968
|
+
if (tools.codex?.available)
|
|
969
|
+
availableProviders.push('openai');
|
|
970
|
+
const intent = String(run.promptAnalysis?.intent?.type || 'execute');
|
|
971
|
+
const risk = String(run.plan?.risk || 'medium');
|
|
972
|
+
const complexity = String(run.plan?.complexity || 'medium');
|
|
973
|
+
run.modelSuggestion = suggestModel(intent, risk, complexity, availableProviders);
|
|
974
|
+
// Warn if model registry is stale
|
|
975
|
+
const age = getRegistryAge();
|
|
976
|
+
if (age > 30 && run.situationBrief) {
|
|
977
|
+
run.situationBrief += '\nWARNING: Model registry is ' + age + ' days old';
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
// models not available
|
|
982
|
+
}
|
|
983
|
+
if (verbose || dryRun) {
|
|
984
|
+
log(formatExecutionPlan(run.plan));
|
|
985
|
+
}
|
|
986
|
+
// Contradiction detection
|
|
987
|
+
if (run.projectBrief && run.plan) {
|
|
988
|
+
try {
|
|
989
|
+
const { detectContradictions } = await import('./intelligence.js');
|
|
990
|
+
const planForCheck = {
|
|
991
|
+
description: run.plan.description || prompt,
|
|
992
|
+
targetFiles: run.plan.targetFiles || run.plan.files || [],
|
|
993
|
+
assumptions: run.plan.assumptions || {}
|
|
994
|
+
};
|
|
995
|
+
run.contradictions = detectContradictions(run.projectBrief, run.taskBrief, planForCheck);
|
|
996
|
+
// Any blocking contradiction fails the pipeline
|
|
997
|
+
const blockers = run.contradictions.filter((c) => c.severity === 'block');
|
|
998
|
+
if (blockers.length > 0) {
|
|
999
|
+
run.completedAt = Date.now();
|
|
1000
|
+
try {
|
|
1001
|
+
const { recordEvent } = await import('./doctor.js');
|
|
1002
|
+
recordEvent({ type: 'contradiction_caught', severity: 'fail', outcome: 'blocked', evidence: blockers.map((b) => b.message).join('; ').slice(0, 200), sessionId: run.id }, cwd);
|
|
1003
|
+
}
|
|
1004
|
+
catch { /* non-blocking */ }
|
|
1005
|
+
return {
|
|
1006
|
+
success: false,
|
|
1007
|
+
gateFailure: 'contradiction',
|
|
1008
|
+
reason: blockers.map((b) => b.message).join('; '),
|
|
1009
|
+
contradictions: blockers,
|
|
1010
|
+
run
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
// contradiction detection failed — continue (degraded)
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Gate 2: Planning gate
|
|
1019
|
+
if (!runGate(run, 'planning', planningGate)) {
|
|
1020
|
+
run.completedAt = Date.now();
|
|
1021
|
+
try {
|
|
1022
|
+
const { recordEvent } = await import('./doctor.js');
|
|
1023
|
+
recordEvent({ type: 'gate_failure', checkId: 'planning-gate', severity: 'fail', outcome: 'blocked', evidence: run.gates.planning.reason, sessionId: run.id }, cwd);
|
|
1024
|
+
}
|
|
1025
|
+
catch { /* non-blocking */ }
|
|
1026
|
+
return { success: false, gateFailure: 'planning', reason: run.gates.planning.reason, run };
|
|
1027
|
+
}
|
|
1028
|
+
// Gate 3: Principle gate
|
|
1029
|
+
if (!runGate(run, 'principle', principleGate)) {
|
|
1030
|
+
run.completedAt = Date.now();
|
|
1031
|
+
try {
|
|
1032
|
+
const { recordEvent } = await import('./doctor.js');
|
|
1033
|
+
recordEvent({ type: 'gate_failure', checkId: 'principle-gate', severity: 'fail', outcome: 'blocked', evidence: run.gates.principle.reason, sessionId: run.id }, cwd);
|
|
1034
|
+
}
|
|
1035
|
+
catch { /* non-blocking */ }
|
|
1036
|
+
return { success: false, gateFailure: 'principle', reason: run.gates.principle.reason, run };
|
|
1037
|
+
}
|
|
1038
|
+
if (dryRun) {
|
|
1039
|
+
run.completedAt = Date.now();
|
|
1040
|
+
return {
|
|
1041
|
+
plan: run.plan,
|
|
1042
|
+
result: null,
|
|
1043
|
+
verification: null,
|
|
1044
|
+
run,
|
|
1045
|
+
projectBrief: run.projectBrief,
|
|
1046
|
+
contradictions: run.contradictions,
|
|
1047
|
+
promptAnalysis: run.promptAnalysis,
|
|
1048
|
+
environment: run.environment,
|
|
1049
|
+
modelSuggestion: run.modelSuggestion,
|
|
1050
|
+
thinkResult: run.thinkResult,
|
|
1051
|
+
decisionPreflight: run.decisionPreflight,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
// Gate 4: Execution gate (cleared to work?)
|
|
1055
|
+
if (!runGate(run, 'execution', executionGate)) {
|
|
1056
|
+
run.completedAt = Date.now();
|
|
1057
|
+
try {
|
|
1058
|
+
const { recordEvent } = await import('./doctor.js');
|
|
1059
|
+
recordEvent({ type: 'gate_failure', checkId: 'execution-gate', severity: 'fail', outcome: 'blocked', evidence: run.gates.execution.reason, sessionId: run.id }, cwd);
|
|
1060
|
+
}
|
|
1061
|
+
catch { /* non-blocking */ }
|
|
1062
|
+
return { success: false, gateFailure: 'execution', reason: run.gates.execution.reason, run };
|
|
1063
|
+
}
|
|
1064
|
+
// ── Phase 3: Execute ──────────────────────────────────────────────────────
|
|
1065
|
+
// Checkpoint (best-effort, before execute).
|
|
1066
|
+
if (run.plan.checkpointRequired) {
|
|
1067
|
+
await createCheckpoint(cwd, run.context);
|
|
1068
|
+
}
|
|
1069
|
+
const detectedRisk = run.context?.detection?.risk ?? 'low';
|
|
1070
|
+
if (detectedRisk === 'high' || detectedRisk === 'critical') {
|
|
1071
|
+
try {
|
|
1072
|
+
const { createCheckpoint: cpCreate } = await import('./checkpoint.js');
|
|
1073
|
+
const cpLabel = `before: ${prompt.slice(0, 80)}`;
|
|
1074
|
+
const cpResult = cpCreate(cpLabel, { cwd });
|
|
1075
|
+
run.checkpoint = cpResult;
|
|
1076
|
+
if (verbose)
|
|
1077
|
+
log(`[pipeline] checkpoint created: ${cpResult.id} (${cpResult.success ? 'ok' : 'failed'})`);
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
// checkpoint.mjs unavailable — non-blocking
|
|
1081
|
+
run.checkpoint = null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
let decision = { ...run.plan._decision };
|
|
1085
|
+
// ── Pre-dispatch think (Position 1: context intelligence) ────────────────
|
|
1086
|
+
{
|
|
1087
|
+
const thinkRefinement = await preDispatchThink(effectivePrompt, files, decision, cwd, (run.context?.profile ?? {}), { log, _skipPreDispatchThink: options._skipPreDispatchThink });
|
|
1088
|
+
if (thinkRefinement.refined) {
|
|
1089
|
+
run._thinkRefinedPrompt = thinkRefinement.prompt;
|
|
1090
|
+
run._thinkRefinedFiles = thinkRefinement.files;
|
|
1091
|
+
decision = thinkRefinement.decision;
|
|
1092
|
+
// Record the think→work handoff for cross-agent context continuity
|
|
1093
|
+
try {
|
|
1094
|
+
const { createHandoff } = await import('./handoff.js');
|
|
1095
|
+
createHandoff('thinker', 'worker', {
|
|
1096
|
+
objective: thinkRefinement.prompt,
|
|
1097
|
+
files: thinkRefinement.files,
|
|
1098
|
+
criteria: thinkRefinement.decision?.criteria || [],
|
|
1099
|
+
confidence: thinkRefinement.confidence,
|
|
1100
|
+
}, run.id || Date.now().toString(36), cwd);
|
|
1101
|
+
}
|
|
1102
|
+
catch { /* non-blocking */ }
|
|
1103
|
+
// Cascade: if think agent is highly confident and task is simple, downgrade worker model
|
|
1104
|
+
if (thinkRefinement.decision) {
|
|
1105
|
+
const thinkConf = thinkRefinement.confidence || 0;
|
|
1106
|
+
const currentModel = decision.model || 'sonnet';
|
|
1107
|
+
if (thinkConf >= 0.9 && currentModel !== 'haiku') {
|
|
1108
|
+
const prevModel = decision.model;
|
|
1109
|
+
decision.model = 'haiku';
|
|
1110
|
+
if (verbose || run?.verbose)
|
|
1111
|
+
process.stderr.write(`[dual-brain] cascade: think confidence ${thinkConf} → downgraded ${prevModel || 'sonnet'} to haiku\n`);
|
|
1112
|
+
}
|
|
1113
|
+
else if (thinkConf >= 0.75 && currentModel === 'opus') {
|
|
1114
|
+
decision.model = 'sonnet';
|
|
1115
|
+
if (verbose || run?.verbose)
|
|
1116
|
+
process.stderr.write(`[dual-brain] cascade: think confidence ${thinkConf} → downgraded opus to sonnet\n`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// Strategy selection — may override dispatch pattern
|
|
1122
|
+
try {
|
|
1123
|
+
const { selectStrategy } = await import('./strategy.js');
|
|
1124
|
+
const strategyResult = selectStrategy(run.context.detection, decision, run.context.profile);
|
|
1125
|
+
if (strategyResult.strategy !== 'direct') {
|
|
1126
|
+
decision._strategy = strategyResult.strategy;
|
|
1127
|
+
decision._strategyReason = strategyResult.reason;
|
|
1128
|
+
if (verbose)
|
|
1129
|
+
process.stderr.write(`[dual-brain] strategy: ${strategyResult.strategy} (${strategyResult.reason})\n`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch { /* non-blocking */ }
|
|
1133
|
+
// Resolve the (possibly refined) prompt and file list for dispatch
|
|
1134
|
+
const dispatchPrompt = run._thinkRefinedPrompt ?? effectivePrompt;
|
|
1135
|
+
const dispatchFiles = run._thinkRefinedFiles ?? files;
|
|
1136
|
+
// ── HEAD judgment injection into agent prompts ─────────────────────────────
|
|
1137
|
+
let headJudgmentBlock = '';
|
|
1138
|
+
if (run.headJudgment) {
|
|
1139
|
+
const hj = run.headJudgment;
|
|
1140
|
+
const hjResult = hj.result;
|
|
1141
|
+
const hjLines = ['[HEAD JUDGMENT]'];
|
|
1142
|
+
// Critical obligations the agent must respect
|
|
1143
|
+
const criticalObs = (hjResult?.obligations || []).filter(o => o.priority === 'critical' || o.priority === 'high');
|
|
1144
|
+
if (criticalObs.length > 0) {
|
|
1145
|
+
hjLines.push('Obligations:');
|
|
1146
|
+
for (const o of criticalObs)
|
|
1147
|
+
hjLines.push(`- ${o.description}`);
|
|
1148
|
+
}
|
|
1149
|
+
// Uncertainties the agent should verify
|
|
1150
|
+
const gaps = (hj.uncertainties || []).filter(u => u.confidence < 0.6);
|
|
1151
|
+
if (gaps.length > 0) {
|
|
1152
|
+
hjLines.push('Verify these (HEAD is uncertain):');
|
|
1153
|
+
for (const g of gaps)
|
|
1154
|
+
hjLines.push(`- ${g.claim} (confidence: ${Math.round(g.confidence * 100)}%) — ${g.wouldChangeIf}`);
|
|
1155
|
+
}
|
|
1156
|
+
// Noticings the agent should be aware of
|
|
1157
|
+
const surfaced = hjResult?.surfaceNoticings || [];
|
|
1158
|
+
if (surfaced.length > 0) {
|
|
1159
|
+
hjLines.push('HEAD noticed:');
|
|
1160
|
+
for (const n of surfaced)
|
|
1161
|
+
hjLines.push(`- ${n.observation}`);
|
|
1162
|
+
}
|
|
1163
|
+
hjLines.push('[/HEAD JUDGMENT]');
|
|
1164
|
+
if (hjLines.length > 2) {
|
|
1165
|
+
headJudgmentBlock = hjLines.join('\n');
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// Collaborative dispatch
|
|
1169
|
+
const collab = await getCollab();
|
|
1170
|
+
const useCollaboration = collab && (run.plan.useChallenger ||
|
|
1171
|
+
detectedRisk === 'high' || detectedRisk === 'critical');
|
|
1172
|
+
if (useCollaboration && collab) {
|
|
1173
|
+
const session = collab.createSession(run.id, effectivePrompt, {
|
|
1174
|
+
crossReview: run.plan.useChallenger,
|
|
1175
|
+
});
|
|
1176
|
+
// Register primary agent
|
|
1177
|
+
const primaryId = `primary-${run.id.slice(0, 8)}`;
|
|
1178
|
+
collab.registerAgent(session, primaryId, 'implementer', decision.provider, decision.model);
|
|
1179
|
+
collab.startAgent(session, primaryId);
|
|
1180
|
+
// Inject collaboration context + HEAD judgment into prompt
|
|
1181
|
+
const collabContext = collab.buildAgentContext(session, primaryId);
|
|
1182
|
+
const promptParts = [collabContext, headJudgmentBlock, dispatchPrompt].filter(Boolean);
|
|
1183
|
+
const collabPrompt = promptParts.join('\n\n');
|
|
1184
|
+
run.result = await dispatch({
|
|
1185
|
+
decision,
|
|
1186
|
+
prompt: collabPrompt,
|
|
1187
|
+
files: dispatchFiles,
|
|
1188
|
+
cwd,
|
|
1189
|
+
dryRun: false,
|
|
1190
|
+
verbose,
|
|
1191
|
+
profile: run.context.profile,
|
|
1192
|
+
situationBrief: run.situationBrief ?? undefined,
|
|
1193
|
+
modelSuggestion: run.modelSuggestion,
|
|
1194
|
+
});
|
|
1195
|
+
// Record agent completion
|
|
1196
|
+
const resultObj = run.result;
|
|
1197
|
+
collab.completeAgent(session, primaryId, run.result, resultObj?.summary);
|
|
1198
|
+
// Extract findings from result
|
|
1199
|
+
if (resultObj?.filesChanged?.length) {
|
|
1200
|
+
for (const f of resultObj.filesChanged)
|
|
1201
|
+
collab.trackFile(session, f, primaryId);
|
|
1202
|
+
}
|
|
1203
|
+
// Cross-review: symmetric — works Claude→OpenAI and OpenAI→Claude
|
|
1204
|
+
const availableProviders = [];
|
|
1205
|
+
const profile = run.context?.profile;
|
|
1206
|
+
const providers = profile?.providers;
|
|
1207
|
+
if (providers?.claude?.enabled !== false)
|
|
1208
|
+
availableProviders.push('claude');
|
|
1209
|
+
const openaiP = providers?.openai;
|
|
1210
|
+
if (openaiP?.enabled && openaiP?.plan)
|
|
1211
|
+
availableProviders.push('openai');
|
|
1212
|
+
if (run.plan.useChallenger && run.plan.challengerModel && resultObj?.status === 'completed') {
|
|
1213
|
+
const reviewSpec = collab.buildCrossReviewPrompt(session, primaryId, availableProviders);
|
|
1214
|
+
if (reviewSpec) {
|
|
1215
|
+
const reviewId = `reviewer-${run.id.slice(0, 8)}`;
|
|
1216
|
+
collab.registerAgent(session, reviewId, 'cross-reviewer', reviewSpec.provider, reviewSpec.model || run.plan.challengerModel);
|
|
1217
|
+
collab.startAgent(session, reviewId);
|
|
1218
|
+
try {
|
|
1219
|
+
const reviewResult = await dispatch({
|
|
1220
|
+
decision: { provider: reviewSpec.provider, model: reviewSpec.model || run.plan.challengerModel, tier: 'search' },
|
|
1221
|
+
prompt: reviewSpec.prompt,
|
|
1222
|
+
files,
|
|
1223
|
+
cwd,
|
|
1224
|
+
dryRun: false,
|
|
1225
|
+
verbose,
|
|
1226
|
+
profile: run.context.profile,
|
|
1227
|
+
situationBrief: run.situationBrief ?? undefined,
|
|
1228
|
+
});
|
|
1229
|
+
collab.completeAgent(session, reviewId, reviewResult, reviewResult?.summary);
|
|
1230
|
+
}
|
|
1231
|
+
catch {
|
|
1232
|
+
collab.completeAgent(session, reviewId, { error: 'review dispatch failed' });
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Synthesize and attach to run
|
|
1237
|
+
run.collaboration = collab.synthesize(session);
|
|
1238
|
+
// Persist collaboration session
|
|
1239
|
+
try {
|
|
1240
|
+
collab.saveSession(session, cwd);
|
|
1241
|
+
}
|
|
1242
|
+
catch { }
|
|
1243
|
+
try {
|
|
1244
|
+
collab.persistEvents(session, cwd);
|
|
1245
|
+
}
|
|
1246
|
+
catch { }
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
const directPrompt = headJudgmentBlock
|
|
1250
|
+
? `${headJudgmentBlock}\n\n${dispatchPrompt}`
|
|
1251
|
+
: dispatchPrompt;
|
|
1252
|
+
run.result = await dispatch({
|
|
1253
|
+
decision,
|
|
1254
|
+
prompt: directPrompt,
|
|
1255
|
+
files: dispatchFiles,
|
|
1256
|
+
cwd,
|
|
1257
|
+
dryRun: false,
|
|
1258
|
+
verbose,
|
|
1259
|
+
profile: run.context.profile,
|
|
1260
|
+
situationBrief: run.situationBrief ?? undefined,
|
|
1261
|
+
modelSuggestion: run.modelSuggestion,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
// Update ledger task with result
|
|
1265
|
+
if (run.taskId) {
|
|
1266
|
+
const { updateTask, failTask } = await import('./ledger.js');
|
|
1267
|
+
const ledgerCwd = options.cwd || process.cwd();
|
|
1268
|
+
const resultObj = run.result;
|
|
1269
|
+
if (resultObj && !resultObj.error) {
|
|
1270
|
+
updateTask(run.taskId, {
|
|
1271
|
+
status: 'done',
|
|
1272
|
+
result: typeof run.result === 'string' ? run.result : JSON.stringify(run.result).slice(0, 500),
|
|
1273
|
+
proof: run.verification ? 'Pipeline verification passed' : 'Execution completed',
|
|
1274
|
+
files: resultObj.filesChanged || run.plan?.targetFiles || []
|
|
1275
|
+
}, ledgerCwd);
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
try {
|
|
1279
|
+
failTask(run.taskId, resultObj?.error || 'Pipeline execution failed', ledgerCwd);
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
// failTask failure is non-blocking
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
// Record action in living docs
|
|
1287
|
+
try {
|
|
1288
|
+
const { appendAction } = await import('./living-docs.js');
|
|
1289
|
+
const docsCwd = options.cwd || process.cwd();
|
|
1290
|
+
const resultObj = run.result;
|
|
1291
|
+
appendAction({
|
|
1292
|
+
type: trigger || 'task',
|
|
1293
|
+
intent: prompt,
|
|
1294
|
+
status: (resultObj && !resultObj.error) ? 'done' : 'failed',
|
|
1295
|
+
owner: 'head',
|
|
1296
|
+
files: resultObj?.filesChanged || run.plan?.targetFiles || [],
|
|
1297
|
+
proof: run.verification ? JSON.stringify(run.verification).slice(0, 200) : null,
|
|
1298
|
+
result: typeof run.result === 'string' ? run.result.slice(0, 300) : null
|
|
1299
|
+
}, docsCwd);
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
// living docs not available — non-blocking
|
|
1303
|
+
}
|
|
1304
|
+
// ── Phase 4: Verification ─────────────────────────────────────────────────
|
|
1305
|
+
run.verification = await verify(run.result, run.plan, cwd);
|
|
1306
|
+
if (verbose) {
|
|
1307
|
+
log(`[pipeline] verification: ${run.verification.ok ? 'ok' : 'failed'}`);
|
|
1308
|
+
for (const note of run.verification.notes)
|
|
1309
|
+
log(`[pipeline] ${note}`);
|
|
1310
|
+
}
|
|
1311
|
+
if (!run.verification.ok) {
|
|
1312
|
+
_incrementFailureCache(prompt);
|
|
1313
|
+
}
|
|
1314
|
+
// Track cost after verification (fail-silent — advisory only)
|
|
1315
|
+
try {
|
|
1316
|
+
const { trackCost } = await import('./cost-tracker.js');
|
|
1317
|
+
const resultObj = run.result;
|
|
1318
|
+
const usage = resultObj?.usage;
|
|
1319
|
+
const tokensUsed = resultObj?.tokensUsed;
|
|
1320
|
+
const tokensEstimated = (usage?.inputTokens ?? tokensUsed?.input ?? 0) +
|
|
1321
|
+
(usage?.outputTokens ?? tokensUsed?.output ?? 0);
|
|
1322
|
+
trackCost({
|
|
1323
|
+
action: trigger || 'execute',
|
|
1324
|
+
model: resultObj?.model ?? run.plan?._decision?.model ?? 'default',
|
|
1325
|
+
tier: run.plan?.tier ?? 'standard',
|
|
1326
|
+
tokensEstimated,
|
|
1327
|
+
wasCacheHit: false,
|
|
1328
|
+
tokensSaved: 0,
|
|
1329
|
+
}, cwd);
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
// cost-tracker not available — non-blocking
|
|
1333
|
+
}
|
|
1334
|
+
// Living docs: update state after significant execution (fail-silent — advisory only)
|
|
1335
|
+
try {
|
|
1336
|
+
const { updateState } = await import('./living-docs.js');
|
|
1337
|
+
const docsCwd = options.cwd || process.cwd();
|
|
1338
|
+
const resultObj = run.result;
|
|
1339
|
+
const successFlag = resultObj && !resultObj.error && run.verification.ok;
|
|
1340
|
+
const stateEntry = `# Current State\n\nLast run: ${new Date().toISOString()}\n` +
|
|
1341
|
+
`Task: ${prompt.slice(0, 120)}\n` +
|
|
1342
|
+
`Status: ${successFlag ? 'completed' : 'failed'}\n` +
|
|
1343
|
+
`Tier: ${run.plan?.tier ?? 'unknown'}\n` +
|
|
1344
|
+
`Model: ${run.plan?.primaryModel ?? 'unknown'}\n`;
|
|
1345
|
+
updateState(stateEntry, docsCwd);
|
|
1346
|
+
}
|
|
1347
|
+
catch {
|
|
1348
|
+
// living-docs not available — non-blocking
|
|
1349
|
+
}
|
|
1350
|
+
// Doctor: record execution outcome event (fail-silent)
|
|
1351
|
+
try {
|
|
1352
|
+
const { recordEvent } = await import('./doctor.js');
|
|
1353
|
+
const resultObj = run.result;
|
|
1354
|
+
const successFlag = resultObj && !resultObj.error && run.verification?.ok;
|
|
1355
|
+
recordEvent({
|
|
1356
|
+
type: successFlag ? 'execution_success' : 'gate_failure',
|
|
1357
|
+
checkId: 'execution',
|
|
1358
|
+
severity: successFlag ? 'pass' : 'fail',
|
|
1359
|
+
outcome: successFlag ? 'pass' : 'fail',
|
|
1360
|
+
evidence: successFlag
|
|
1361
|
+
? `Completed ${trigger}: ${prompt.slice(0, 100)}`
|
|
1362
|
+
: (resultObj?.error || 'Execution failed'),
|
|
1363
|
+
sessionId: run.id,
|
|
1364
|
+
}, cwd);
|
|
1365
|
+
}
|
|
1366
|
+
catch { /* non-blocking */ }
|
|
1367
|
+
// Doctor: record learning from this execution outcome (fail-silent)
|
|
1368
|
+
try {
|
|
1369
|
+
const { recordLearning } = await import('./doctor.js');
|
|
1370
|
+
const doctorCwd = options.cwd || process.cwd();
|
|
1371
|
+
const resultObj = run.result;
|
|
1372
|
+
const successFlag = resultObj && !resultObj.error && run.verification.ok;
|
|
1373
|
+
recordLearning({
|
|
1374
|
+
taskType: run.context?.detection?.intent ?? 'unknown',
|
|
1375
|
+
prompt,
|
|
1376
|
+
model: resultObj?.model ?? run.plan?._decision?.model ?? '',
|
|
1377
|
+
provider: resultObj?.provider ?? run.plan?.primaryProvider ?? '',
|
|
1378
|
+
tier: run.plan?.tier ?? '',
|
|
1379
|
+
reasoningDepth: run.plan?.reasoningDepth ?? 'low',
|
|
1380
|
+
wasEnriched: !!run.enrichedPrompt,
|
|
1381
|
+
wasDualBrain: !!(run.plan?.useChallenger && run.plan?.challengerModel),
|
|
1382
|
+
success: !!successFlag,
|
|
1383
|
+
duration: run.completedAt ? (Date.now() - run.startedAt) : 0,
|
|
1384
|
+
filesChanged: (resultObj?.filesChanged ?? []).length,
|
|
1385
|
+
}, doctorCwd);
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
// doctor not available — non-blocking
|
|
1389
|
+
}
|
|
1390
|
+
// ── Phase 5: Outcome ──────────────────────────────────────────────────────
|
|
1391
|
+
await recordOutcomeSafe(run);
|
|
1392
|
+
// Gate 5: Outcome gate
|
|
1393
|
+
if (!runGate(run, 'outcome', outcomeGate)) {
|
|
1394
|
+
run.completedAt = Date.now();
|
|
1395
|
+
return { success: false, gateFailure: 'outcome', reason: run.gates.outcome.reason, run };
|
|
1396
|
+
}
|
|
1397
|
+
// Provider-aware compaction survival
|
|
1398
|
+
try {
|
|
1399
|
+
const { buildSurvivalBlock } = await import('./provider-context.js');
|
|
1400
|
+
const resultObj = run.result;
|
|
1401
|
+
const effectiveProvider = resultObj?.provider || run.plan?.primaryProvider || 'claude';
|
|
1402
|
+
const survivalKit = buildSurvivalBlock(effectiveProvider, {
|
|
1403
|
+
activeTask: prompt.slice(0, 120),
|
|
1404
|
+
provider: effectiveProvider,
|
|
1405
|
+
model: resultObj?.model || run.plan?.primaryModel,
|
|
1406
|
+
tier: run.plan?.tier,
|
|
1407
|
+
risk: run.context?.detection?.risk,
|
|
1408
|
+
filesInProgress: resultObj?.filesChanged || [],
|
|
1409
|
+
decisions: (run.collaboration?.decisions?.map(d => String(d.decision)) || []),
|
|
1410
|
+
warnings: run.contradictions?.map((c) => c.message) || [],
|
|
1411
|
+
routingRules: [
|
|
1412
|
+
`provider=${effectiveProvider}`,
|
|
1413
|
+
`model=${resultObj?.model || run.plan?.primaryModel}`,
|
|
1414
|
+
`tier=${run.plan?.tier}`,
|
|
1415
|
+
],
|
|
1416
|
+
});
|
|
1417
|
+
if (run.situationBrief) {
|
|
1418
|
+
run.situationBrief = `${survivalKit}\n\n${run.situationBrief}`;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
catch { /* non-blocking */ }
|
|
1422
|
+
// Post-session receipt
|
|
1423
|
+
try {
|
|
1424
|
+
const { generateReceipt } = await import('./receipt.js');
|
|
1425
|
+
generateReceipt(run, cwd);
|
|
1426
|
+
}
|
|
1427
|
+
catch { /* non-blocking */ }
|
|
1428
|
+
// Persist decision for future recall
|
|
1429
|
+
const resultObj = run.result;
|
|
1430
|
+
if (resultObj && !resultObj?.error) {
|
|
1431
|
+
try {
|
|
1432
|
+
const { persistDecision } = await import('./think-engine.js');
|
|
1433
|
+
const teCwd = options.cwd || process.cwd();
|
|
1434
|
+
persistDecision(prompt, typeof run.result === 'string' ? run.result : JSON.stringify(run.result).slice(0, 1000), String(run.thinkResult?.tier || 'standard'), { tags: options.tags || [], projectBrief: run.projectBrief }, teCwd);
|
|
1435
|
+
}
|
|
1436
|
+
catch {
|
|
1437
|
+
// persist failed — non-blocking
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
// Provider-aware continuity handoff
|
|
1441
|
+
try {
|
|
1442
|
+
const { generateHandoff, saveHandoff, pruneHandoffs } = await import('./continuity.js');
|
|
1443
|
+
const { generateProviderHandoff } = await import('./provider-context.js');
|
|
1444
|
+
const handoffCwd = options.cwd || process.cwd();
|
|
1445
|
+
const handoffProvider = resultObj?.provider || run.plan?.primaryProvider || 'claude';
|
|
1446
|
+
const sessionState = {
|
|
1447
|
+
taskDescription: prompt.slice(0, 200),
|
|
1448
|
+
filesChanged: resultObj?.filesChanged || run.plan?.targetFiles || [],
|
|
1449
|
+
testsRun: run.verification?.notes || [],
|
|
1450
|
+
decisions: run.plan ? [{
|
|
1451
|
+
provider: run.plan.primaryProvider,
|
|
1452
|
+
model: run.plan.primaryModel,
|
|
1453
|
+
}] : [],
|
|
1454
|
+
unresolved: run.contradictions?.filter((c) => c.severity !== 'block').map((c) => c.message) || [],
|
|
1455
|
+
routingHistory: {
|
|
1456
|
+
lastProvider: handoffProvider,
|
|
1457
|
+
lastModel: resultObj?.model || run.plan?.primaryModel || undefined,
|
|
1458
|
+
failedProviders: resultObj?.error ? [run.plan?.primaryProvider].filter((p) => !!p) : [],
|
|
1459
|
+
},
|
|
1460
|
+
resumeHint: resultObj && !resultObj?.error
|
|
1461
|
+
? undefined
|
|
1462
|
+
: `retry: ${prompt.slice(0, 100)}`,
|
|
1463
|
+
};
|
|
1464
|
+
// Save both standard + provider-aware handoff
|
|
1465
|
+
const handoff = generateProviderHandoff(sessionState, handoffProvider);
|
|
1466
|
+
saveHandoff(handoff, handoffCwd);
|
|
1467
|
+
pruneHandoffs(handoffCwd, 10);
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
// continuity is best-effort — never block pipeline completion
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
catch (err) {
|
|
1474
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1475
|
+
log(`[pipeline] error in pipeline step: ${message}`);
|
|
1476
|
+
run.result = { status: 'error', error: message };
|
|
1477
|
+
run.verification = { ok: false, notes: [message] };
|
|
1478
|
+
if (run.context)
|
|
1479
|
+
_incrementFailureCache(prompt);
|
|
1480
|
+
run.completedAt = Date.now();
|
|
1481
|
+
return { success: false, gateFailure: 'error', reason: message, run };
|
|
1482
|
+
}
|
|
1483
|
+
run.completedAt = Date.now();
|
|
1484
|
+
// Return both new-style and legacy-compatible shapes
|
|
1485
|
+
return {
|
|
1486
|
+
success: true,
|
|
1487
|
+
run,
|
|
1488
|
+
headJudgment: run.headJudgment,
|
|
1489
|
+
projectBrief: run.projectBrief,
|
|
1490
|
+
contradictions: run.contradictions,
|
|
1491
|
+
promptAnalysis: run.promptAnalysis,
|
|
1492
|
+
environment: run.environment,
|
|
1493
|
+
modelSuggestion: run.modelSuggestion,
|
|
1494
|
+
thinkResult: run.thinkResult,
|
|
1495
|
+
decisionPreflight: run.decisionPreflight,
|
|
1496
|
+
checkpoint: run.checkpoint,
|
|
1497
|
+
collaboration: run.collaboration,
|
|
1498
|
+
plan: run.plan,
|
|
1499
|
+
result: run.result,
|
|
1500
|
+
verification: run.verification,
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
//# sourceMappingURL=pipeline.js.map
|