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