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