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.
Files changed (309) hide show
  1. package/.dual-brain/docs/claude-code-extension-points.md +32 -0
  2. package/.dual-brain/docs/data-tools-capabilities.md +181 -0
  3. package/.dual-brain/docs/ecosystem-tools.md +91 -0
  4. package/.dual-brain/docs/panel-handoff.md +124 -0
  5. package/.dual-brain/docs/ruflo-analysis.md +48 -0
  6. package/bin/dual-brain.mjs +56 -56
  7. package/dist/mcp-server/index.d.ts +27 -0
  8. package/dist/mcp-server/index.js +359 -0
  9. package/dist/mcp-server/index.js.map +1 -0
  10. package/dist/src/agent-protocol.d.ts +163 -0
  11. package/dist/src/agent-protocol.js +368 -0
  12. package/dist/src/agent-protocol.js.map +1 -0
  13. package/dist/src/agents/registry.d.ts +52 -0
  14. package/dist/src/agents/registry.js +393 -0
  15. package/dist/src/agents/registry.js.map +1 -0
  16. package/dist/src/awareness.d.ts +93 -0
  17. package/dist/src/awareness.js +406 -0
  18. package/dist/src/awareness.js.map +1 -0
  19. package/dist/src/brief.d.ts +48 -0
  20. package/dist/src/brief.js +179 -0
  21. package/dist/src/brief.js.map +1 -0
  22. package/dist/src/calibration.d.ts +32 -0
  23. package/dist/src/calibration.js +133 -0
  24. package/dist/src/calibration.js.map +1 -0
  25. package/dist/src/checkpoint.d.ts +33 -0
  26. package/dist/src/checkpoint.js +99 -0
  27. package/dist/src/checkpoint.js.map +1 -0
  28. package/dist/src/ci-triage.d.ts +33 -0
  29. package/dist/src/ci-triage.js +193 -0
  30. package/dist/src/ci-triage.js.map +1 -0
  31. package/dist/src/cognitive-loop.d.ts +56 -0
  32. package/dist/src/cognitive-loop.js +495 -0
  33. package/dist/src/cognitive-loop.js.map +1 -0
  34. package/dist/src/collaboration.d.ts +147 -0
  35. package/dist/src/collaboration.js +438 -0
  36. package/dist/src/collaboration.js.map +1 -0
  37. package/dist/src/context-intel.d.ts +47 -0
  38. package/dist/src/context-intel.js +156 -0
  39. package/dist/src/context-intel.js.map +1 -0
  40. package/dist/src/context.d.ts +53 -0
  41. package/dist/src/context.js +332 -0
  42. package/dist/src/context.js.map +1 -0
  43. package/dist/src/continuity.d.ts +89 -0
  44. package/dist/src/continuity.js +230 -0
  45. package/dist/src/continuity.js.map +1 -0
  46. package/dist/src/cost-tracker.d.ts +47 -0
  47. package/dist/src/cost-tracker.js +170 -0
  48. package/dist/src/cost-tracker.js.map +1 -0
  49. package/dist/src/debrief.d.ts +53 -0
  50. package/dist/src/debrief.js +222 -0
  51. package/dist/src/debrief.js.map +1 -0
  52. package/dist/src/decide.d.ts +96 -0
  53. package/dist/src/decide.js +744 -0
  54. package/dist/src/decide.js.map +1 -0
  55. package/dist/src/decompose.d.ts +39 -0
  56. package/dist/src/decompose.js +218 -0
  57. package/dist/src/decompose.js.map +1 -0
  58. package/dist/src/detect.d.ts +91 -0
  59. package/dist/src/detect.js +544 -0
  60. package/dist/src/detect.js.map +1 -0
  61. package/dist/src/dispatch.d.ts +154 -0
  62. package/dist/src/dispatch.js +1306 -0
  63. package/dist/src/dispatch.js.map +1 -0
  64. package/dist/src/doctor.d.ts +421 -0
  65. package/dist/src/doctor.js +1689 -0
  66. package/dist/src/doctor.js.map +1 -0
  67. package/dist/src/engine.d.ts +70 -0
  68. package/dist/src/engine.js +155 -0
  69. package/dist/src/engine.js.map +1 -0
  70. package/dist/src/envelope.d.ts +36 -0
  71. package/dist/src/envelope.js +80 -0
  72. package/dist/src/envelope.js.map +1 -0
  73. package/dist/src/failure-memory.d.ts +55 -0
  74. package/dist/src/failure-memory.js +175 -0
  75. package/dist/src/failure-memory.js.map +1 -0
  76. package/dist/src/fx.d.ts +87 -0
  77. package/dist/src/fx.js +272 -0
  78. package/dist/src/fx.js.map +1 -0
  79. package/dist/src/governance.d.ts +93 -0
  80. package/dist/src/governance.js +261 -0
  81. package/dist/src/governance.js.map +1 -0
  82. package/dist/src/handoff.d.ts +11 -0
  83. package/dist/src/handoff.js +90 -0
  84. package/dist/src/handoff.js.map +1 -0
  85. package/dist/src/head-protocol.d.ts +76 -0
  86. package/dist/src/head-protocol.js +109 -0
  87. package/dist/src/head-protocol.js.map +1 -0
  88. package/dist/src/head.d.ts +222 -0
  89. package/dist/src/head.js +765 -0
  90. package/dist/src/head.js.map +1 -0
  91. package/dist/src/health.d.ts +132 -0
  92. package/dist/src/health.js +435 -0
  93. package/dist/src/health.js.map +1 -0
  94. package/dist/src/inbox.d.ts +70 -0
  95. package/dist/src/inbox.js +218 -0
  96. package/dist/src/inbox.js.map +1 -0
  97. package/dist/src/index.d.ts +33 -0
  98. package/dist/src/index.js +38 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/install-hooks.d.ts +13 -0
  101. package/dist/src/install-hooks.js +88 -0
  102. package/dist/src/install-hooks.js.map +1 -0
  103. package/dist/src/integrity.d.ts +59 -0
  104. package/dist/src/integrity.js +206 -0
  105. package/dist/src/integrity.js.map +1 -0
  106. package/dist/src/intelligence.d.ts +104 -0
  107. package/dist/src/intelligence.js +391 -0
  108. package/dist/src/intelligence.js.map +1 -0
  109. package/dist/src/ledger.d.ts +54 -0
  110. package/dist/src/ledger.js +179 -0
  111. package/dist/src/ledger.js.map +1 -0
  112. package/dist/src/living-docs.d.ts +14 -0
  113. package/dist/src/living-docs.js +197 -0
  114. package/dist/src/living-docs.js.map +1 -0
  115. package/dist/src/memory-tiers.d.ts +37 -0
  116. package/dist/src/memory-tiers.js +160 -0
  117. package/dist/src/memory-tiers.js.map +1 -0
  118. package/dist/src/model-profiles.d.ts +65 -0
  119. package/dist/src/model-profiles.js +568 -0
  120. package/dist/src/model-profiles.js.map +1 -0
  121. package/dist/src/models.d.ts +58 -0
  122. package/dist/src/models.js +327 -0
  123. package/dist/src/models.js.map +1 -0
  124. package/dist/src/narrative.d.ts +54 -0
  125. package/dist/src/narrative.js +163 -0
  126. package/dist/src/narrative.js.map +1 -0
  127. package/dist/src/nextstep.d.ts +16 -0
  128. package/dist/src/nextstep.js +103 -0
  129. package/dist/src/nextstep.js.map +1 -0
  130. package/dist/src/observer.d.ts +18 -0
  131. package/dist/src/observer.js +251 -0
  132. package/dist/src/observer.js.map +1 -0
  133. package/dist/src/outcome.d.ts +110 -0
  134. package/dist/src/outcome.js +377 -0
  135. package/dist/src/outcome.js.map +1 -0
  136. package/dist/src/pipeline.d.ts +167 -0
  137. package/dist/src/pipeline.js +1503 -0
  138. package/dist/src/pipeline.js.map +1 -0
  139. package/dist/src/playbook.d.ts +59 -0
  140. package/dist/src/playbook.js +238 -0
  141. package/dist/src/playbook.js.map +1 -0
  142. package/dist/src/pr-agent.d.ts +97 -0
  143. package/dist/src/pr-agent.js +195 -0
  144. package/dist/src/pr-agent.js.map +1 -0
  145. package/dist/src/predictive.d.ts +57 -0
  146. package/dist/src/predictive.js +230 -0
  147. package/dist/src/predictive.js.map +1 -0
  148. package/dist/src/profile.d.ts +294 -0
  149. package/dist/src/profile.js +1347 -0
  150. package/dist/src/profile.js.map +1 -0
  151. package/dist/src/prompt-audit.d.ts +22 -0
  152. package/dist/src/prompt-audit.js +194 -0
  153. package/dist/src/prompt-audit.js.map +1 -0
  154. package/dist/src/prompt-intel.d.ts +12 -0
  155. package/dist/src/prompt-intel.js +321 -0
  156. package/dist/src/prompt-intel.js.map +1 -0
  157. package/dist/src/provider-context.d.ts +121 -0
  158. package/dist/src/provider-context.js +222 -0
  159. package/dist/src/provider-context.js.map +1 -0
  160. package/dist/src/provider-manager.d.ts +92 -0
  161. package/dist/src/provider-manager.js +428 -0
  162. package/dist/src/provider-manager.js.map +1 -0
  163. package/dist/src/receipt.d.ts +87 -0
  164. package/dist/src/receipt.js +326 -0
  165. package/dist/src/receipt.js.map +1 -0
  166. package/dist/src/recommendations.d.ts +13 -0
  167. package/dist/src/recommendations.js +291 -0
  168. package/dist/src/recommendations.js.map +1 -0
  169. package/dist/src/redact.d.ts +15 -0
  170. package/dist/src/redact.js +129 -0
  171. package/dist/src/redact.js.map +1 -0
  172. package/dist/src/replit.d.ts +397 -0
  173. package/dist/src/replit.js +1160 -0
  174. package/dist/src/replit.js.map +1 -0
  175. package/dist/src/repo.d.ts +149 -0
  176. package/dist/src/repo.js +416 -0
  177. package/dist/src/repo.js.map +1 -0
  178. package/dist/src/revert.d.ts +30 -0
  179. package/dist/src/revert.js +166 -0
  180. package/dist/src/revert.js.map +1 -0
  181. package/dist/src/room.d.ts +102 -0
  182. package/dist/src/room.js +212 -0
  183. package/dist/src/room.js.map +1 -0
  184. package/dist/src/routing-advisor.d.ts +57 -0
  185. package/dist/src/routing-advisor.js +221 -0
  186. package/dist/src/routing-advisor.js.map +1 -0
  187. package/dist/src/self-correct.d.ts +40 -0
  188. package/dist/src/self-correct.js +137 -0
  189. package/dist/src/self-correct.js.map +1 -0
  190. package/dist/src/session-lock.d.ts +35 -0
  191. package/dist/src/session-lock.js +134 -0
  192. package/dist/src/session-lock.js.map +1 -0
  193. package/dist/src/session.d.ts +267 -0
  194. package/dist/src/session.js +1660 -0
  195. package/dist/src/session.js.map +1 -0
  196. package/dist/src/settings-tui.d.ts +5 -0
  197. package/dist/src/settings-tui.js +422 -0
  198. package/dist/src/settings-tui.js.map +1 -0
  199. package/dist/src/setup-flow.d.ts +63 -0
  200. package/dist/src/setup-flow.js +233 -0
  201. package/dist/src/setup-flow.js.map +1 -0
  202. package/dist/src/signal.d.ts +19 -0
  203. package/dist/src/signal.js +122 -0
  204. package/dist/src/signal.js.map +1 -0
  205. package/dist/src/simmer.d.ts +85 -0
  206. package/dist/src/simmer.js +224 -0
  207. package/dist/src/simmer.js.map +1 -0
  208. package/dist/src/state-export.d.ts +129 -0
  209. package/dist/src/state-export.js +233 -0
  210. package/dist/src/state-export.js.map +1 -0
  211. package/dist/src/strategy.d.ts +54 -0
  212. package/dist/src/strategy.js +95 -0
  213. package/dist/src/strategy.js.map +1 -0
  214. package/dist/src/subscription.d.ts +40 -0
  215. package/dist/src/subscription.js +189 -0
  216. package/dist/src/subscription.js.map +1 -0
  217. package/dist/src/templates.d.ts +208 -0
  218. package/dist/src/templates.js +238 -0
  219. package/dist/src/templates.js.map +1 -0
  220. package/dist/src/test.d.ts +9 -0
  221. package/dist/src/test.js +1173 -0
  222. package/dist/src/test.js.map +1 -0
  223. package/dist/src/think-engine.d.ts +67 -0
  224. package/dist/src/think-engine.js +412 -0
  225. package/dist/src/think-engine.js.map +1 -0
  226. package/dist/src/tui.d.ts +71 -0
  227. package/dist/src/tui.js +242 -0
  228. package/dist/src/tui.js.map +1 -0
  229. package/dist/src/types.d.ts +177 -0
  230. package/dist/src/types.js +6 -0
  231. package/dist/src/types.js.map +1 -0
  232. package/dist/src/update-check.d.ts +7 -0
  233. package/dist/src/update-check.js +36 -0
  234. package/dist/src/update-check.js.map +1 -0
  235. package/dist/src/wave-planner.d.ts +30 -0
  236. package/dist/src/wave-planner.js +281 -0
  237. package/dist/src/wave-planner.js.map +1 -0
  238. package/hooks/head-guard.sh +41 -0
  239. package/hooks/task-classifier.mjs +328 -0
  240. package/hooks/vibe-router.mjs +387 -0
  241. package/package.json +29 -153
  242. package/src/agents/registry.mjs +0 -405
  243. package/src/awareness.mjs +0 -425
  244. package/src/brief.mjs +0 -266
  245. package/src/calibration.mjs +0 -148
  246. package/src/checkpoint.mjs +0 -109
  247. package/src/ci-triage.mjs +0 -191
  248. package/src/cognitive-loop.mjs +0 -562
  249. package/src/collaboration.mjs +0 -545
  250. package/src/context-intel.mjs +0 -158
  251. package/src/context.mjs +0 -389
  252. package/src/continuity.mjs +0 -298
  253. package/src/cost-tracker.mjs +0 -184
  254. package/src/debrief.mjs +0 -228
  255. package/src/decide.mjs +0 -1099
  256. package/src/decompose.mjs +0 -331
  257. package/src/detect.mjs +0 -702
  258. package/src/dispatch.mjs +0 -1447
  259. package/src/doctor.mjs +0 -1607
  260. package/src/envelope.mjs +0 -139
  261. package/src/failure-memory.mjs +0 -178
  262. package/src/fx.mjs +0 -276
  263. package/src/governance.mjs +0 -279
  264. package/src/handoff.mjs +0 -87
  265. package/src/head-protocol.mjs +0 -128
  266. package/src/head.mjs +0 -952
  267. package/src/health.mjs +0 -528
  268. package/src/inbox.mjs +0 -195
  269. package/src/index.mjs +0 -44
  270. package/src/install-hooks.mjs +0 -100
  271. package/src/integrity.mjs +0 -245
  272. package/src/intelligence.mjs +0 -447
  273. package/src/ledger.mjs +0 -196
  274. package/src/living-docs.mjs +0 -210
  275. package/src/memory-tiers.mjs +0 -193
  276. package/src/models.mjs +0 -363
  277. package/src/narrative.mjs +0 -169
  278. package/src/nextstep.mjs +0 -100
  279. package/src/observer.mjs +0 -241
  280. package/src/outcome.mjs +0 -400
  281. package/src/pipeline.mjs +0 -1711
  282. package/src/playbook.mjs +0 -257
  283. package/src/pr-agent.mjs +0 -214
  284. package/src/predictive.mjs +0 -250
  285. package/src/profile.mjs +0 -1411
  286. package/src/prompt-audit.mjs +0 -231
  287. package/src/prompt-intel.mjs +0 -325
  288. package/src/provider-context.mjs +0 -257
  289. package/src/receipt.mjs +0 -344
  290. package/src/recommendations.mjs +0 -296
  291. package/src/redact.mjs +0 -192
  292. package/src/replit.mjs +0 -1210
  293. package/src/repo.mjs +0 -445
  294. package/src/revert.mjs +0 -149
  295. package/src/routing-advisor.mjs +0 -204
  296. package/src/self-correct.mjs +0 -147
  297. package/src/session-lock.mjs +0 -160
  298. package/src/session.mjs +0 -1655
  299. package/src/settings-tui.mjs +0 -373
  300. package/src/setup-flow.mjs +0 -223
  301. package/src/signal.mjs +0 -115
  302. package/src/simmer.mjs +0 -241
  303. package/src/strategy.mjs +0 -235
  304. package/src/subscription.mjs +0 -212
  305. package/src/templates.mjs +0 -260
  306. package/src/think-engine.mjs +0 -428
  307. package/src/tui.mjs +0 -276
  308. package/src/update-check.mjs +0 -35
  309. 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
- }