opencandle 0.10.0 → 0.11.1

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 (223) hide show
  1. package/README.md +12 -9
  2. package/dist/analysts/orchestrator.d.ts +3 -4
  3. package/dist/analysts/orchestrator.js +34 -44
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/cli-main.js +242 -9
  6. package/dist/cli-main.js.map +1 -1
  7. package/dist/config.d.ts +4 -9
  8. package/dist/config.js +7 -10
  9. package/dist/config.js.map +1 -1
  10. package/dist/doctor/report.js +1 -1
  11. package/dist/doctor/report.js.map +1 -1
  12. package/dist/infra/cache.d.ts +6 -0
  13. package/dist/infra/cache.js +16 -6
  14. package/dist/infra/cache.js.map +1 -1
  15. package/dist/infra/freshness.d.ts +21 -0
  16. package/dist/infra/freshness.js +119 -0
  17. package/dist/infra/freshness.js.map +1 -0
  18. package/dist/infra/market-calendar.d.ts +14 -0
  19. package/dist/infra/market-calendar.js +137 -0
  20. package/dist/infra/market-calendar.js.map +1 -0
  21. package/dist/infra/rate-limiter.js +1 -0
  22. package/dist/infra/rate-limiter.js.map +1 -1
  23. package/dist/market-state/daily-report.d.ts +1 -1
  24. package/dist/market-state/daily-report.js +5 -16
  25. package/dist/market-state/daily-report.js.map +1 -1
  26. package/dist/market-state/service.d.ts +0 -35
  27. package/dist/market-state/service.js +0 -63
  28. package/dist/market-state/service.js.map +1 -1
  29. package/dist/market-state/summaries.d.ts +8 -0
  30. package/dist/market-state/summaries.js +70 -0
  31. package/dist/market-state/summaries.js.map +1 -0
  32. package/dist/memory/sqlite.js +20 -19
  33. package/dist/memory/sqlite.js.map +1 -1
  34. package/dist/onboarding/providers.d.ts +25 -1
  35. package/dist/onboarding/providers.js +26 -0
  36. package/dist/onboarding/providers.js.map +1 -1
  37. package/dist/pi/opencandle-extension.js +57 -226
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-action-dedupe.d.ts +6 -0
  40. package/dist/pi/session-action-dedupe.js +126 -0
  41. package/dist/pi/session-action-dedupe.js.map +1 -0
  42. package/dist/pi/session-writer-lock.d.ts +26 -2
  43. package/dist/pi/session-writer-lock.js +230 -18
  44. package/dist/pi/session-writer-lock.js.map +1 -1
  45. package/dist/pi/setup.js +5 -5
  46. package/dist/pi/setup.js.map +1 -1
  47. package/dist/pi/tui-session-coordinator.d.ts +15 -0
  48. package/dist/pi/tui-session-coordinator.js +283 -0
  49. package/dist/pi/tui-session-coordinator.js.map +1 -0
  50. package/dist/prompts/context-builder.js +1 -1
  51. package/dist/prompts/policy-cards.js +1 -1
  52. package/dist/prompts/policy-cards.js.map +1 -1
  53. package/dist/providers/alpha-vantage.js +6 -0
  54. package/dist/providers/alpha-vantage.js.map +1 -1
  55. package/dist/providers/coingecko.js +1 -0
  56. package/dist/providers/coingecko.js.map +1 -1
  57. package/dist/providers/polymarket.d.ts +2 -0
  58. package/dist/providers/polymarket.js +163 -0
  59. package/dist/providers/polymarket.js.map +1 -0
  60. package/dist/providers/wrap-provider.js +5 -3
  61. package/dist/providers/wrap-provider.js.map +1 -1
  62. package/dist/providers/yahoo-finance.d.ts +3 -0
  63. package/dist/providers/yahoo-finance.js +206 -10
  64. package/dist/providers/yahoo-finance.js.map +1 -1
  65. package/dist/routing/classify-intent.js +4 -5
  66. package/dist/routing/classify-intent.js.map +1 -1
  67. package/dist/routing/entity-extractor.js +16 -0
  68. package/dist/routing/entity-extractor.js.map +1 -1
  69. package/dist/routing/route-manifest.js +1 -2
  70. package/dist/routing/route-manifest.js.map +1 -1
  71. package/dist/routing/router-llm-client.js +18 -3
  72. package/dist/routing/router-llm-client.js.map +1 -1
  73. package/dist/routing/router-prompt.js +1 -1
  74. package/dist/routing/router.d.ts +1 -1
  75. package/dist/routing/router.js +309 -9
  76. package/dist/routing/router.js.map +1 -1
  77. package/dist/routing/slot-resolver.d.ts +1 -0
  78. package/dist/routing/slot-resolver.js +1 -1
  79. package/dist/routing/slot-resolver.js.map +1 -1
  80. package/dist/runtime/evidence.d.ts +1 -0
  81. package/dist/runtime/evidence.js.map +1 -1
  82. package/dist/runtime/numeric-claims.d.ts +23 -0
  83. package/dist/runtime/numeric-claims.js +100 -0
  84. package/dist/runtime/numeric-claims.js.map +1 -0
  85. package/dist/runtime/planning-evidence.js +1 -76
  86. package/dist/runtime/planning-evidence.js.map +1 -1
  87. package/dist/runtime/prompt-step.d.ts +10 -1
  88. package/dist/runtime/prompt-step.js +116 -2
  89. package/dist/runtime/prompt-step.js.map +1 -1
  90. package/dist/runtime/session-coordinator.d.ts +11 -1
  91. package/dist/runtime/session-coordinator.js +354 -62
  92. package/dist/runtime/session-coordinator.js.map +1 -1
  93. package/dist/runtime/validation.d.ts +3 -1
  94. package/dist/runtime/workflow-events.d.ts +1 -1
  95. package/dist/runtime/workflow-events.js.map +1 -1
  96. package/dist/runtime/workflow-types.d.ts +3 -0
  97. package/dist/runtime/workflow-types.js.map +1 -1
  98. package/dist/system-prompt.js +1 -1
  99. package/dist/tools/fundamentals/comps.d.ts +1 -0
  100. package/dist/tools/fundamentals/comps.js +20 -9
  101. package/dist/tools/fundamentals/comps.js.map +1 -1
  102. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  103. package/dist/tools/fundamentals/dcf.js +190 -76
  104. package/dist/tools/fundamentals/dcf.js.map +1 -1
  105. package/dist/tools/index.d.ts +1 -1
  106. package/dist/tools/index.js +3 -3
  107. package/dist/tools/index.js.map +1 -1
  108. package/dist/tools/macro/event-probabilities.d.ts +8 -0
  109. package/dist/tools/macro/event-probabilities.js +121 -0
  110. package/dist/tools/macro/event-probabilities.js.map +1 -0
  111. package/dist/tools/market/crypto-price.d.ts +4 -1
  112. package/dist/tools/market/crypto-price.js +10 -1
  113. package/dist/tools/market/crypto-price.js.map +1 -1
  114. package/dist/tools/market/screen-stocks.js +14 -3
  115. package/dist/tools/market/screen-stocks.js.map +1 -1
  116. package/dist/tools/market/stock-quote.d.ts +4 -1
  117. package/dist/tools/market/stock-quote.js +9 -4
  118. package/dist/tools/market/stock-quote.js.map +1 -1
  119. package/dist/tools/options/option-chain.d.ts +4 -1
  120. package/dist/tools/options/option-chain.js +12 -1
  121. package/dist/tools/options/option-chain.js.map +1 -1
  122. package/dist/tools/portfolio/daily-report.js +10 -4
  123. package/dist/tools/portfolio/daily-report.js.map +1 -1
  124. package/dist/tools/portfolio/risk-analysis.js +1 -1
  125. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  126. package/dist/tools/technical/backtest.d.ts +23 -5
  127. package/dist/tools/technical/backtest.js +131 -94
  128. package/dist/tools/technical/backtest.js.map +1 -1
  129. package/dist/tools/technical/indicators.js +1 -1
  130. package/dist/tools/technical/indicators.js.map +1 -1
  131. package/dist/types/fundamentals.d.ts +1 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/market.d.ts +2 -0
  134. package/dist/types/options.d.ts +1 -0
  135. package/dist/types/prediction-markets.d.ts +13 -0
  136. package/dist/types/prediction-markets.js +2 -0
  137. package/dist/types/prediction-markets.js.map +1 -0
  138. package/gui/server/chat-event-adapter.ts +89 -5
  139. package/gui/server/http-routes.ts +661 -37
  140. package/gui/server/invoke-tool.ts +165 -16
  141. package/gui/server/live-chat-event-adapter.ts +37 -2
  142. package/gui/server/local-session-coordinator.ts +97 -0
  143. package/gui/server/market-state-api.ts +64 -48
  144. package/gui/server/projector.ts +132 -3
  145. package/gui/server/server.ts +51 -6
  146. package/gui/server/session-actions.ts +146 -45
  147. package/gui/server/session-entry-wait.ts +22 -3
  148. package/gui/server/tool-metadata.ts +3 -1
  149. package/gui/server/ws-hub.ts +77 -10
  150. package/gui/shared/chat-events.ts +10 -1
  151. package/gui/shared/event-reducer.ts +1 -0
  152. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
  153. package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
  154. package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
  155. package/gui/web/dist/index.html +2 -2
  156. package/package.json +13 -15
  157. package/src/analysts/orchestrator.ts +35 -56
  158. package/src/cli-main.ts +253 -13
  159. package/src/config.ts +12 -20
  160. package/src/doctor/report.ts +1 -1
  161. package/src/infra/cache.ts +24 -11
  162. package/src/infra/freshness.ts +165 -0
  163. package/src/infra/market-calendar.ts +193 -0
  164. package/src/infra/rate-limiter.ts +1 -0
  165. package/src/market-state/daily-report.ts +6 -16
  166. package/src/market-state/service.ts +0 -136
  167. package/src/market-state/summaries.ts +75 -0
  168. package/src/memory/sqlite.ts +23 -19
  169. package/src/onboarding/providers.ts +29 -1
  170. package/src/pi/opencandle-extension.ts +67 -270
  171. package/src/pi/session-action-dedupe.ts +155 -0
  172. package/src/pi/session-writer-lock.ts +290 -20
  173. package/src/pi/setup.ts +6 -6
  174. package/src/pi/tui-session-coordinator.ts +351 -0
  175. package/src/prompts/context-builder.ts +1 -1
  176. package/src/prompts/policy-cards.ts +1 -1
  177. package/src/providers/alpha-vantage.ts +6 -0
  178. package/src/providers/coingecko.ts +2 -0
  179. package/src/providers/polymarket.ts +214 -0
  180. package/src/providers/wrap-provider.ts +5 -3
  181. package/src/providers/yahoo-finance.ts +339 -27
  182. package/src/routing/classify-intent.ts +4 -5
  183. package/src/routing/entity-extractor.ts +17 -0
  184. package/src/routing/route-manifest.ts +1 -2
  185. package/src/routing/router-llm-client.ts +31 -19
  186. package/src/routing/router-prompt.ts +1 -1
  187. package/src/routing/router.ts +379 -8
  188. package/src/routing/slot-resolver.ts +1 -1
  189. package/src/runtime/evidence.ts +1 -0
  190. package/src/runtime/numeric-claims.ts +108 -0
  191. package/src/runtime/planning-evidence.ts +6 -91
  192. package/src/runtime/prompt-step.ts +127 -2
  193. package/src/runtime/session-coordinator.ts +464 -74
  194. package/src/runtime/validation.ts +1 -1
  195. package/src/runtime/workflow-events.ts +2 -0
  196. package/src/runtime/workflow-types.ts +3 -0
  197. package/src/system-prompt.ts +1 -1
  198. package/src/tools/AGENTS.md +1 -1
  199. package/src/tools/fundamentals/comps.ts +25 -14
  200. package/src/tools/fundamentals/dcf.ts +217 -87
  201. package/src/tools/index.ts +3 -3
  202. package/src/tools/macro/event-probabilities.ts +141 -0
  203. package/src/tools/market/crypto-price.ts +14 -2
  204. package/src/tools/market/screen-stocks.ts +14 -3
  205. package/src/tools/market/stock-quote.ts +49 -43
  206. package/src/tools/options/option-chain.ts +16 -2
  207. package/src/tools/portfolio/daily-report.ts +10 -4
  208. package/src/tools/portfolio/risk-analysis.ts +1 -1
  209. package/src/tools/technical/backtest.ts +167 -108
  210. package/src/tools/technical/indicators.ts +1 -1
  211. package/src/types/fundamentals.ts +1 -0
  212. package/src/types/index.ts +1 -0
  213. package/src/types/market.ts +2 -0
  214. package/src/types/options.ts +1 -0
  215. package/src/types/prediction-markets.ts +13 -0
  216. package/dist/tools/portfolio/predictions.d.ts +0 -55
  217. package/dist/tools/portfolio/predictions.js +0 -422
  218. package/dist/tools/portfolio/predictions.js.map +0 -1
  219. package/gui/server/chat-run-session.ts +0 -16
  220. package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
  221. package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
  222. package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
  223. package/src/tools/portfolio/predictions.ts +0 -553
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
- <script type="module" crossorigin src="/assets/index-B7QAjY5g.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-D5dbWPfM.css">
11
+ <script type="module" crossorigin src="/assets/index-DtqMMBTr.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-D4F9AJnn.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencandle",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Financial trading & investing agent",
5
5
  "license": "MIT",
6
6
  "homepage": "https://opencandle.app",
@@ -14,7 +14,9 @@
14
14
  "type": "module",
15
15
  "workspaces": [
16
16
  "gui/server",
17
- "gui/web"
17
+ "gui/web",
18
+ "packages/ui",
19
+ "website"
18
20
  ],
19
21
  "bin": {
20
22
  "opencandle": "dist/cli.js"
@@ -86,7 +88,7 @@
86
88
  "prestart": "npm run check:node",
87
89
  "start": "tsx src/cli.ts",
88
90
  "gui:web:build": "npm --workspace @opencandle/gui-web run build",
89
- "docs:site:build": "node website/build.mjs",
91
+ "docs:site:build": "npm --workspace @opencandle/website run build",
90
92
  "docs:site:serve": "node website/serve.mjs",
91
93
  "docs:links:check": "node scripts/check-public-doc-links.mjs",
92
94
  "gui": "tsx gui/server/server.ts",
@@ -103,15 +105,12 @@
103
105
  "test:e2e:credential-soft-fallback": "tsx tests/e2e/credential-soft-fallback.test.ts",
104
106
  "test:e2e:credential-per-workflow-cap": "tsx tests/e2e/credential-per-workflow-cap.test.ts",
105
107
  "test:gui:browser": "OPENCANDLE_GUI_BROWSER=1 vitest run --config vitest.config.gui.ts",
108
+ "test:gui:release-smoke": "npm run gui:web:build && OPENCANDLE_GUI_RELEASE_SMOKE=1 vitest run --config vitest.config.gui-release.ts",
106
109
  "test:e2e:providers": "tsx tests/e2e/providers.test.ts",
107
- "test:evals": "vitest run --config vitest.config.evals.ts",
108
- "eval:router-live": "tsx tests/scripts/run-live-router-eval.ts",
109
- "test:evals:usually": "EVAL_TIER=usually vitest run --config vitest.config.evals.ts",
110
- "test:evals:product": "tsx tests/scripts/run-product-evals.ts",
111
- "test:evals:competitive": "tsx tests/scripts/run-competitive-finance-eval.ts",
110
+ "test:e2e:harness-dcf": "tsx tests/e2e/harness-dcf.test.ts",
111
+ "eval": "tsx tests/scripts/run-evals.ts",
112
112
  "test:packed-install": "node scripts/packed-install-smoke.mjs",
113
- "eval:competitive:analyze": "tsx tests/scripts/analyze-competitive-finance-report.ts",
114
- "review:pr": ".agents/skills/autoreview/scripts/autoreview --mode branch --prompt-file .agents/skills/autoreview/references/opencandle-review.md --parallel-tests \"npx tsc --noEmit && npx vitest run\"",
113
+ "review:pr": ".agents/skills/autoreview/scripts/autoreview --mode branch --prompt-file .agents/skills/autoreview/references/opencandle-review.md --parallel-tests \"npx tsc --noEmit && npx biome ci . && npx vitest run\"",
115
114
  "package:contents:check": "node scripts/check-package-contents.mjs",
116
115
  "version:patch": "npm version patch --no-git-tag-version",
117
116
  "version:minor": "npm version minor --no-git-tag-version",
@@ -119,7 +118,7 @@
119
118
  "version:set": "npm version",
120
119
  "prepublishOnly": "npm run release:check",
121
120
  "publish:dry": "npm run release:check && npm publish --access public --dry-run",
122
- "release:check": "npx tsc --noEmit && npx biome ci . && npm test && npm run docs:site:build && npm run package:contents:check && npm run test:packed-install && npm run docs:links:check",
121
+ "release:check": "npx tsc --noEmit && npx biome ci . && npm test && npm run test:gui:release-smoke && npm run docs:site:build && npm run package:contents:check && npm run test:packed-install && npm run docs:links:check",
123
122
  "release:patch": "node scripts/release.mjs patch",
124
123
  "release:minor": "node scripts/release.mjs minor",
125
124
  "release:major": "node scripts/release.mjs major"
@@ -140,17 +139,16 @@
140
139
  "@sinclair/typebox": "*"
141
140
  },
142
141
  "devDependencies": {
143
- "@agentclientprotocol/claude-agent-acp": "^0.52.0",
142
+ "@agentclientprotocol/claude-agent-acp": "^0.55.0",
144
143
  "@agentclientprotocol/codex-acp": "^1.0.1",
145
144
  "@biomejs/biome": "^2.5.1",
146
145
  "@sinclair/typebox": "^0.34.49",
147
146
  "@types/better-sqlite3": "^7.6.13",
148
147
  "@types/node": "^26.0.1",
149
- "acpx": "^0.11.2",
148
+ "acpx": "^0.12.0",
150
149
  "playwright-core": "^1.61.1",
151
150
  "typescript": "^6.0.3",
152
151
  "vite": "^8.1.0",
153
- "vitest": "^4.1.9",
154
- "vitest-evals": "^0.14.0"
152
+ "vitest": "^4.1.9"
155
153
  }
156
154
  }
@@ -1,5 +1,7 @@
1
1
  import type { WorkflowDefinition } from "../runtime/prompt-step.js";
2
2
  import { promptStep } from "../runtime/prompt-step.js";
3
+ import type { AnalystOutput } from "../runtime/workflow-types.js";
4
+ import { isAnalystSplit, tallyVotes } from "./contracts.js";
3
5
 
4
6
  export type AnalystRole = "valuation" | "momentum" | "options" | "contrarian" | "risk";
5
7
 
@@ -115,7 +117,24 @@ CONCESSIONS: [bullet list of points you concede]
115
117
  REMAINING CONVICTION: [1-10, where 10 = fully confident despite bear case]`;
116
118
  }
117
119
 
120
+ export function buildAnalystVoteTallyBlock(outputs: AnalystOutput[]): string | null {
121
+ if (outputs.length < 2) return null;
122
+ const tally = tallyVotes(outputs);
123
+ const split = isAnalystSplit(outputs);
124
+ return `## Deterministic Analyst Vote Tally
125
+ - Parsed analyst outputs: ${outputs.length}
126
+ - BUY: ${tally.buy}
127
+ - HOLD: ${tally.hold}
128
+ - SELL: ${tally.sell}
129
+ - Weighted average conviction: ${tally.weightedConviction}
130
+ - Computed verdict: ${tally.verdict}
131
+ - BUY+SELL split: ${split ? "yes" : "no"}`;
132
+ }
133
+
118
134
  export function buildSynthesisPrompt(symbol: string): string {
135
+ // The runtime tally block is injected at dispatch time by
136
+ // SessionCoordinator.prepareWorkflowPrompt, where parsed analyst outputs
137
+ // exist; this builder runs at workflow-definition time and stays static.
119
138
  return `**[Synthesis]** You have received five analyst signals with conviction scores for ${symbol}, a bull case arguing FOR the position, and a bear case arguing AGAINST.
120
139
  If a bull rebuttal with concessions appears above (not a line starting with "REBUTTAL SKIPPED"), treat the concessions as validated risks that must be addressed.
121
140
 
@@ -148,34 +167,11 @@ Additionally check:
148
167
 
149
168
  Output: VALIDATED if all checks pass, or list specific corrections needed.`;
150
169
 
151
- const SYNTHESIS_PROMPT_NO_DEBATE = (symbol: string) =>
152
- `**[Synthesis]** You have received five analyst signals above for ${symbol}. Tally the SIGNAL votes (BUY/HOLD/SELL) and weight them by CONVICTION scores. Then provide:
153
- 1. **Vote Tally**: X BUY, Y HOLD, Z SELL — weighted average conviction
154
- 2. **Verdict**: Buy, Hold, or Sell — based on the signal consensus
155
- 3. **Key thesis** in 2-3 sentences
156
- 4. **Bull case** — what could go right
157
- 5. **Bear case** — what could go wrong
158
- 6. **Key levels** — entry, stop-loss, and target prices
159
- 7. **Position sizing recommendation** based on risk profile
160
-
161
- Be direct and actionable. This is your final word on ${symbol}.`;
162
-
163
- const VALIDATION_PROMPT_NO_DEBATE = (symbol: string) =>
164
- `**[Validation Check]** Review your complete analysis of ${symbol} above. For each specific number you cited (price, P/E, revenue, RSI, intrinsic value, etc.), verify it matches the tool output data you received. Flag any inconsistencies. If you stated a number without fetching it first, call that out as UNVERIFIED. Output: VALIDATED if all numbers check out, or list specific corrections needed.`;
165
-
166
170
  export function getInitialAnalysisPrompt(symbol: string): string {
167
171
  return `Begin comprehensive analysis of ${symbol}. Start by getting the current stock quote.`;
168
172
  }
169
173
 
170
- export interface ComprehensiveAnalysisOptions {
171
- debate?: boolean;
172
- }
173
-
174
- export function buildComprehensiveAnalysisDefinition(
175
- symbol: string,
176
- options?: ComprehensiveAnalysisOptions,
177
- ): WorkflowDefinition {
178
- const debate = options?.debate ?? true;
174
+ export function buildComprehensiveAnalysisDefinition(symbol: string): WorkflowDefinition {
179
175
  const roles: AnalystRole[] = ["valuation", "momentum", "options", "contrarian", "risk"];
180
176
 
181
177
  const analystOutputs = roles.map((r) => `${r}_signal`);
@@ -193,45 +189,28 @@ export function buildComprehensiveAnalysisDefinition(
193
189
  ),
194
190
  ];
195
191
 
196
- if (debate) {
197
- return {
198
- workflowType: "comprehensive_analysis",
199
- steps: [
200
- ...analystSteps,
201
- promptStep("debate_bull", "Bull researcher case", buildBullPrompt(symbol), {
202
- requiredInputs: analystOutputs,
203
- expectedOutputs: ["bull_thesis"],
204
- }),
205
- promptStep("debate_bear", "Bear researcher case", buildBearPrompt(symbol), {
206
- requiredInputs: [...analystOutputs, "bull_thesis"],
207
- expectedOutputs: ["bear_thesis"],
208
- }),
209
- promptStep("debate_rebuttal", "Bull rebuttal (self-gating)", buildRebuttalPrompt(symbol), {
210
- requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis"],
211
- expectedOutputs: ["rebuttal"],
212
- }),
213
- promptStep("synthesis", "Resolve the debate", buildSynthesisPrompt(symbol), {
214
- requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis", "rebuttal"],
215
- expectedOutputs: ["verdict"],
216
- }),
217
- promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_DEBATE(symbol), {
218
- skippable: true,
219
- requiredInputs: ["verdict"],
220
- expectedOutputs: ["validation_result"],
221
- }),
222
- ],
223
- };
224
- }
225
-
226
192
  return {
227
193
  workflowType: "comprehensive_analysis",
228
194
  steps: [
229
195
  ...analystSteps,
230
- promptStep("synthesis", "Synthesize analyst signals", SYNTHESIS_PROMPT_NO_DEBATE(symbol), {
196
+ promptStep("debate_bull", "Bull researcher case", buildBullPrompt(symbol), {
231
197
  requiredInputs: analystOutputs,
198
+ expectedOutputs: ["bull_thesis"],
199
+ }),
200
+ promptStep("debate_bear", "Bear researcher case", buildBearPrompt(symbol), {
201
+ requiredInputs: [...analystOutputs, "bull_thesis"],
202
+ expectedOutputs: ["bear_thesis"],
203
+ }),
204
+ promptStep("debate_rebuttal", "Bull rebuttal (self-gating)", buildRebuttalPrompt(symbol), {
205
+ skippable: true,
206
+ requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis"],
207
+ expectedOutputs: ["rebuttal"],
208
+ }),
209
+ promptStep("synthesis", "Resolve the debate", buildSynthesisPrompt(symbol), {
210
+ requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis", "rebuttal"],
232
211
  expectedOutputs: ["verdict"],
233
212
  }),
234
- promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_NO_DEBATE(symbol), {
213
+ promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_DEBATE(symbol), {
235
214
  skippable: true,
236
215
  requiredInputs: ["verdict"],
237
216
  expectedOutputs: ["validation_result"],
package/src/cli-main.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { createRequire } from "node:module";
3
4
  import { dirname, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import {
6
8
  AuthStorage,
@@ -11,6 +13,7 @@ import {
11
13
  InteractiveMode,
12
14
  initTheme,
13
15
  ModelRegistry,
16
+ SessionManager,
14
17
  SettingsManager,
15
18
  } from "@earendil-works/pi-coding-agent";
16
19
  import { loadEnv } from "./config.js";
@@ -19,10 +22,13 @@ import { createOpenCandleSession } from "./pi/session.js";
19
22
  import { continueOpenCandleSession } from "./pi/session-storage.js";
20
23
  import {
21
24
  acquireSessionWriterLock,
25
+ migrateWriterLockScope,
22
26
  refreshSessionWriterLock,
23
27
  releaseSessionWriterLock,
28
+ type WriterLock,
24
29
  writerLockScopeForSession,
25
30
  } from "./pi/session-writer-lock.js";
31
+ import { startTuiSessionCoordinatorServer } from "./pi/tui-session-coordinator.js";
26
32
 
27
33
  const require = createRequire(import.meta.url);
28
34
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -196,22 +202,62 @@ async function main(): Promise<void> {
196
202
  initTheme(settingsManager.getTheme(), true);
197
203
 
198
204
  const sessionManager = continueOpenCandleSession(cwd);
205
+ let activeSessionManager = sessionManager;
199
206
  const sessionWriterLockScope = writerLockScopeForSession(sessionManager);
200
- const sessionWriterLock = await acquireSessionWriterLock(sessionWriterLockScope, "tui");
207
+ let runtime: Awaited<ReturnType<typeof createAgentSessionRuntime>> | undefined;
208
+ const tuiCoordinator = await startTuiSessionCoordinatorServer({
209
+ getSession: () => {
210
+ if (!runtime) throw new Error("OpenCandle is still starting this session.");
211
+ return runtime.session;
212
+ },
213
+ getSessionManager: () => activeSessionManager,
214
+ getModelUnavailableMessage: () => {
215
+ if (!runtime) return "OpenCandle is still starting this session.";
216
+ const session = runtime.session;
217
+ const model = session.model;
218
+ if (!model) {
219
+ return "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
220
+ }
221
+ if (!session.modelRegistry.hasConfiguredAuth(model)) {
222
+ return "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
223
+ }
224
+ return null;
225
+ },
226
+ syncWriterLockScope: () => syncActiveSessionWriterLockScope(),
227
+ });
228
+ const sessionWriterLock = await acquireSessionWriterLock(sessionWriterLockScope, "tui", {
229
+ coordinatorEndpoint: tuiCoordinator.endpoint,
230
+ coordinatorSecret: tuiCoordinator.secret,
231
+ });
201
232
  if (sessionWriterLock.role !== "writer") {
202
- console.error(
203
- `Session is currently being written by ${sessionWriterLock.lock.processKind} (pid ${sessionWriterLock.lock.pid}).`,
204
- );
233
+ await tuiCoordinator.close();
234
+ if (await runFollowerTuiProxy(sessionWriterLock.lock, sessionManager, cwd)) return;
235
+ console.error("OpenCandle is syncing this session in another window. Try again shortly.");
205
236
  process.exitCode = 1;
206
237
  return;
207
238
  }
208
239
  let activeSessionWriterLockScope = sessionWriterLockScope;
209
- const writerLockHeartbeat = setInterval(
210
- () => refreshSessionWriterLock(activeSessionWriterLockScope),
211
- 5000,
212
- );
240
+ let activeSessionWriterLockLost = false;
241
+ function syncActiveSessionWriterLockScope(): void {
242
+ if (activeSessionWriterLockLost) throw new Error("OpenCandle is reconnecting to this session.");
243
+ const nextScope = writerLockScopeForSession(activeSessionManager);
244
+ if (nextScope === activeSessionWriterLockScope) return;
245
+ if (migrateWriterLockScope(activeSessionWriterLockScope, nextScope)) {
246
+ activeSessionWriterLockScope = nextScope;
247
+ } else {
248
+ activeSessionWriterLockLost = true;
249
+ throw new Error("OpenCandle is reconnecting to this session.");
250
+ }
251
+ }
252
+ const writerLockHeartbeat = setInterval(() => {
253
+ try {
254
+ syncActiveSessionWriterLockScope();
255
+ refreshSessionWriterLock(activeSessionWriterLockScope);
256
+ } catch {
257
+ clearInterval(writerLockHeartbeat);
258
+ }
259
+ }, 5000);
213
260
 
214
- let runtime: Awaited<ReturnType<typeof createAgentSessionRuntime>> | undefined;
215
261
  try {
216
262
  runtime = await createAgentSessionRuntime(
217
263
  async (opts) => {
@@ -239,20 +285,28 @@ async function main(): Promise<void> {
239
285
  },
240
286
  { cwd, agentDir, sessionManager },
241
287
  );
288
+ syncActiveSessionWriterLockScope();
242
289
  runtime.setRebindSession(async (nextSession) => {
243
290
  const nextSessionWriterLockScope = writerLockScopeForSession(nextSession.sessionManager);
244
- if (nextSessionWriterLockScope === activeSessionWriterLockScope) return;
291
+ if (nextSessionWriterLockScope === activeSessionWriterLockScope) {
292
+ activeSessionManager = nextSession.sessionManager;
293
+ syncActiveSessionWriterLockScope();
294
+ return;
295
+ }
245
296
  const nextSessionWriterLock = await acquireSessionWriterLock(
246
297
  nextSessionWriterLockScope,
247
298
  "tui",
299
+ {
300
+ coordinatorEndpoint: tuiCoordinator.endpoint,
301
+ coordinatorSecret: tuiCoordinator.secret,
302
+ },
248
303
  );
249
304
  if (nextSessionWriterLock.role !== "writer") {
250
- throw new Error(
251
- `Session is currently being written by ${nextSessionWriterLock.lock.processKind} (pid ${nextSessionWriterLock.lock.pid}).`,
252
- );
305
+ throw new Error("OpenCandle is syncing this session in another window. Try again shortly.");
253
306
  }
254
307
  releaseSessionWriterLock(activeSessionWriterLockScope);
255
308
  activeSessionWriterLockScope = nextSessionWriterLockScope;
309
+ activeSessionManager = nextSession.sessionManager;
256
310
  });
257
311
  const interactiveMode = new InteractiveMode(runtime, {
258
312
  modelFallbackMessage: shouldSuppressFallbackMessage
@@ -263,8 +317,194 @@ async function main(): Promise<void> {
263
317
  } finally {
264
318
  clearInterval(writerLockHeartbeat);
265
319
  releaseSessionWriterLock(activeSessionWriterLockScope);
320
+ await tuiCoordinator.close();
266
321
  await runtime?.dispose();
267
322
  }
268
323
  }
269
324
 
325
+ async function runFollowerTuiProxy(
326
+ lock: WriterLock,
327
+ sessionManager: ReturnType<typeof continueOpenCandleSession>,
328
+ cwd: string,
329
+ ): Promise<boolean> {
330
+ if (!lock.coordinatorEndpoint || !lock.coordinatorSecret) return false;
331
+ if (!process.stdin.isTTY) return false;
332
+
333
+ const input = createInterface({ input: process.stdin, output: process.stdout });
334
+ const follower = startFollowerTranscriptPrinter(sessionManager, cwd);
335
+ try {
336
+ console.log("Connected to the active OpenCandle session. Type /exit to close.");
337
+ while (true) {
338
+ const prompt = (await input.question("> ")).trim();
339
+ if (!prompt) continue;
340
+ if (prompt === "/exit" || prompt === "/quit") break;
341
+ await forwardTuiPrompt(lock, sessionManager.getSessionId(), prompt);
342
+ follower.markSeen();
343
+ }
344
+ } finally {
345
+ follower.stop();
346
+ input.close();
347
+ }
348
+ return true;
349
+ }
350
+
351
+ function startFollowerTranscriptPrinter(
352
+ sessionManager: ReturnType<typeof continueOpenCandleSession>,
353
+ cwd: string,
354
+ ): { stop: () => void; markSeen: () => void } {
355
+ const sessionFile = sessionManager.getSessionFile();
356
+ if (!sessionFile) return { stop: () => {}, markSeen: () => {} };
357
+ const seenEntryIds = new Set(
358
+ sessionManager
359
+ .getEntries()
360
+ .map((entry) => entryId(entry))
361
+ .filter((id): id is string => Boolean(id)),
362
+ );
363
+ const openFreshSession = () =>
364
+ SessionManager.open(sessionFile, sessionManager.getSessionDir(), cwd);
365
+ const markSeen = () => {
366
+ try {
367
+ for (const entry of openFreshSession().getEntries()) {
368
+ const id = entryId(entry);
369
+ if (id) seenEntryIds.add(id);
370
+ }
371
+ } catch {
372
+ // The owner may be rotating the session file; the next poll will catch up.
373
+ }
374
+ };
375
+ let polling = false;
376
+ const poll = () => {
377
+ if (polling) return;
378
+ polling = true;
379
+ try {
380
+ for (const entry of openFreshSession().getEntries()) {
381
+ const id = entryId(entry);
382
+ if (!id || seenEntryIds.has(id)) continue;
383
+ seenEntryIds.add(id);
384
+ const text = followerEntryText(entry);
385
+ if (text) process.stdout.write(`\n${text}\n`);
386
+ }
387
+ } catch {
388
+ // The owner may be rotating the session file; the next poll will catch up.
389
+ } finally {
390
+ polling = false;
391
+ }
392
+ };
393
+ const interval = setInterval(poll, 1000);
394
+ return { stop: () => clearInterval(interval), markSeen };
395
+ }
396
+
397
+ function entryId(entry: unknown): string | null {
398
+ const record = asRecord(entry);
399
+ return typeof record.id === "string" ? record.id : null;
400
+ }
401
+
402
+ function followerEntryText(entry: unknown): string {
403
+ const record = asRecord(entry);
404
+ if (record.type === "message") {
405
+ const message = asRecord(record.message);
406
+ const role = typeof message.role === "string" ? message.role : "message";
407
+ const text = contentText(message.content);
408
+ return text ? `${role}: ${text}` : "";
409
+ }
410
+ if (record.type === "custom") {
411
+ const text = contentText(record.message ?? record.text ?? record.content);
412
+ return text ? `system: ${text}` : "";
413
+ }
414
+ return "";
415
+ }
416
+
417
+ async function forwardTuiPrompt(
418
+ lock: WriterLock,
419
+ sessionId: string,
420
+ prompt: string,
421
+ ): Promise<void> {
422
+ if (!lock.coordinatorEndpoint || !lock.coordinatorSecret) return;
423
+ const response = await fetch(
424
+ new URL("/api/local-coordinator/chat-run", lock.coordinatorEndpoint),
425
+ {
426
+ method: "POST",
427
+ headers: {
428
+ "content-type": "application/json",
429
+ "x-opencandle-coordinator-secret": lock.coordinatorSecret,
430
+ },
431
+ body: JSON.stringify({
432
+ prompt,
433
+ sessionId,
434
+ actionId: `tui-proxy-${randomUUID()}`,
435
+ }),
436
+ },
437
+ );
438
+ if (!response.ok) {
439
+ const body = (await response.json().catch(() => ({}))) as { error?: string };
440
+ console.error(body.error || response.statusText);
441
+ return;
442
+ }
443
+ await printSseResponse(response);
444
+ }
445
+
446
+ async function printSseResponse(response: Response): Promise<void> {
447
+ const reader = response.body?.getReader();
448
+ if (!reader) return;
449
+ const decoder = new TextDecoder();
450
+ let buffer = "";
451
+ while (true) {
452
+ const { value, done } = await reader.read();
453
+ if (done) break;
454
+ buffer += decoder.decode(value, { stream: true });
455
+ let index = buffer.indexOf("\n\n");
456
+ while (index !== -1) {
457
+ const block = buffer.slice(0, index);
458
+ buffer = buffer.slice(index + 2);
459
+ printSseBlock(block);
460
+ index = buffer.indexOf("\n\n");
461
+ }
462
+ }
463
+ }
464
+
465
+ function printSseBlock(block: string): void {
466
+ const data = block
467
+ .split("\n")
468
+ .filter((line) => line.startsWith("data:"))
469
+ .map((line) => line.slice(5).trim())
470
+ .join("\n");
471
+ if (!data) return;
472
+ const event = JSON.parse(data) as {
473
+ type?: string;
474
+ role?: string;
475
+ text?: string;
476
+ content?: Array<{ type?: string; text?: string }>;
477
+ error?: { message?: string };
478
+ };
479
+ const text = sseEventText(event);
480
+ if (event.type === "message.completed" && text) {
481
+ process.stdout.write(`${text}\n`);
482
+ } else if (event.type === "run.failed") {
483
+ process.stderr.write(`${event.error?.message || "Run failed"}\n`);
484
+ }
485
+ }
486
+
487
+ function sseEventText(event: {
488
+ text?: string;
489
+ content?: Array<{ type?: string; text?: string }>;
490
+ }): string {
491
+ if (event.text) return event.text;
492
+ return contentText(event.content);
493
+ }
494
+
495
+ function contentText(content: unknown): string {
496
+ if (typeof content === "string") return content;
497
+ if (!Array.isArray(content)) return "";
498
+ return content
499
+ .filter((part) => part.type === "text" && part.text)
500
+ .map((part) => part.text)
501
+ .join("");
502
+ }
503
+
504
+ function asRecord(value: unknown): Record<string, unknown> {
505
+ return typeof value === "object" && value !== null && !Array.isArray(value)
506
+ ? (value as Record<string, unknown>)
507
+ : {};
508
+ }
509
+
270
510
  await main();
package/src/config.ts CHANGED
@@ -14,7 +14,7 @@ export interface SentimentConfig {
14
14
  maxNotableClaims?: number;
15
15
  }
16
16
 
17
- export type RouterMode = "rules" | "llm";
17
+ export type RouterMode = "llm";
18
18
  export type ToolScopeMode = "observe" | "enforce";
19
19
  export type PlanningMigrationStatuses = Partial<Record<TaskFamily, PlanningBehaviorMode>>;
20
20
 
@@ -24,13 +24,10 @@ export interface Config {
24
24
  braveApiKey?: string;
25
25
  exaApiKey?: string;
26
26
  finnhubApiKey?: string;
27
- /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
28
- debate?: boolean;
29
27
  /**
30
- * Intent-router mode. `"rules"` (default) uses the deterministic rule
31
- * router (`classifyIntent` + `extractPreferences`). `"llm"` opts into the
32
- * LLM router ahead of prompt assembly. Controlled by
33
- * `OPENCANDLE_ROUTER_MODE`.
28
+ * Intent-router mode. The LLM router is the only production routing path;
29
+ * `OPENCANDLE_ROUTER_MODE` accepts only `"llm"` (or unset). The removed
30
+ * `"rules"` value fails startup with migration guidance.
34
31
  */
35
32
  routerMode: RouterMode;
36
33
  /**
@@ -66,8 +63,6 @@ export interface OpenCandleFileConfig {
66
63
  apiKey?: string;
67
64
  };
68
65
  };
69
- /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
70
- debate?: boolean;
71
66
  sentiment?: {
72
67
  retentionDays?: number;
73
68
  defaultSubreddits?: string[];
@@ -95,7 +90,7 @@ export function loadEnv(path = ".env"): void {
95
90
  if (eqIndex === -1) continue;
96
91
  const key = trimmed.slice(0, eqIndex).trim();
97
92
  const value = trimmed.slice(eqIndex + 1).trim();
98
- if (key && value) {
93
+ if (key && value && process.env[key] === undefined) {
99
94
  process.env[key] = value;
100
95
  }
101
96
  }
@@ -141,11 +136,13 @@ const PLANNING_BEHAVIOR_MODES = [
141
136
 
142
137
  function resolveRouterMode(): RouterMode {
143
138
  const raw = process.env.OPENCANDLE_ROUTER_MODE;
144
- if (raw === undefined || raw === "") return "rules";
145
- if (raw === "rules" || raw === "llm") return raw;
146
- throw new Error(
147
- `Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed values: "rules" (default) or "llm".`,
148
- );
139
+ if (raw === undefined || raw === "" || raw === "llm") return "llm";
140
+ if (raw === "rules") {
141
+ throw new Error(
142
+ 'OPENCANDLE_ROUTER_MODE="rules" was removed: the deterministic rules router is no longer a production routing path. Unset OPENCANDLE_ROUTER_MODE to use the LLM router.',
143
+ );
144
+ }
145
+ throw new Error(`Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed value: "llm" (default).`);
149
146
  }
150
147
 
151
148
  function resolveToolScopeMode(): ToolScopeMode {
@@ -191,7 +188,6 @@ function isPlanningBehaviorMode(value: string | undefined): value is PlanningBeh
191
188
  }
192
189
 
193
190
  function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
194
- const debateEnv = process.env.OPENCANDLE_DEBATE;
195
191
  const fileSentiment = fileConfig.sentiment;
196
192
  return {
197
193
  alphaVantageApiKey:
@@ -200,10 +196,6 @@ function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
200
196
  braveApiKey: process.env.BRAVE_API_KEY ?? fileConfig.providers?.brave?.apiKey,
201
197
  exaApiKey: process.env.EXA_API_KEY ?? fileConfig.providers?.exa?.apiKey,
202
198
  finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
203
- debate:
204
- debateEnv !== undefined
205
- ? debateEnv !== "false" && debateEnv !== "0"
206
- : (fileConfig.debate ?? true),
207
199
  routerMode: resolveRouterMode(),
208
200
  toolScopeMode: resolveToolScopeMode(),
209
201
  planningMigrationStatuses: resolvePlanningMigrationStatuses(),
@@ -450,7 +450,7 @@ function providerStatusCheck(status: ProviderStatus, modeOverride?: "session"):
450
450
  summary: ok ? "Reachable" : `Not reachable (${status.state})`,
451
451
  remediation: ok
452
452
  ? undefined
453
- : "Check network connectivity and retry. Core quote and market-data workflows depend on Yahoo Finance reachability.",
453
+ : `Check network connectivity and retry. OpenCandle uses ${provider.displayName} through a public HTTP endpoint.`,
454
454
  metadata: { ...metadata, statusCode: status.statusCode ?? null },
455
455
  };
456
456
  }