dexterm-cli 0.1.7 → 0.2.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 (233) hide show
  1. package/dist/packages/agents/src/spectator.js +31 -3
  2. package/dist/packages/agents/src/tiers.js +12 -0
  3. package/dist/packages/agents/src/tools.js +13 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +7 -2
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +5 -2
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +21 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  30. package/dist/packages/autotrade/src/rugproof.js +231 -0
  31. package/dist/packages/autotrade/src/runner.js +194 -28
  32. package/dist/packages/autotrade/src/service-guard.js +125 -0
  33. package/dist/packages/autotrade/src/service-install.js +215 -0
  34. package/dist/packages/autotrade/src/service-recover.js +232 -0
  35. package/dist/packages/autotrade/src/service.js +289 -0
  36. package/dist/packages/autotrade/src/sessions.js +16 -7
  37. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  38. package/dist/packages/autotrade/src/signal.js +196 -0
  39. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  40. package/dist/packages/autotrade/src/strategy.js +15 -2
  41. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  42. package/dist/packages/classifier/src/calibrate.js +138 -0
  43. package/dist/packages/classifier/src/classify.js +107 -0
  44. package/dist/packages/classifier/src/config.js +220 -0
  45. package/dist/packages/classifier/src/dossier.js +95 -0
  46. package/dist/packages/classifier/src/hardware.js +63 -0
  47. package/dist/packages/classifier/src/index.js +18 -0
  48. package/dist/packages/classifier/src/models.js +111 -0
  49. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  50. package/dist/packages/classifier/src/providers/http.js +63 -0
  51. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  52. package/dist/packages/classifier/src/providers/llm.js +89 -0
  53. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  54. package/dist/packages/classifier/src/questions.js +97 -0
  55. package/dist/packages/classifier/src/registry.js +42 -0
  56. package/dist/packages/classifier/src/snapshot.js +31 -0
  57. package/dist/packages/classifier/src/systemone.js +88 -0
  58. package/dist/packages/classifier/src/types.js +18 -0
  59. package/dist/packages/cli/assets/dash/dash.css +139 -0
  60. package/dist/packages/cli/assets/dash/dash.js +200 -0
  61. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  62. package/dist/packages/cli/assets/dash/index.html +36 -0
  63. package/dist/packages/cli/src/analyst/backend.js +3 -2
  64. package/dist/packages/cli/src/analyst/codex.js +3 -2
  65. package/dist/packages/cli/src/app-help-gen.js +26 -2
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  68. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
  70. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  71. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  72. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  73. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  74. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  75. package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
  76. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  77. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  78. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  79. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  80. package/dist/packages/cli/src/classifier-live.js +81 -0
  81. package/dist/packages/cli/src/dash/data.js +217 -0
  82. package/dist/packages/cli/src/dash/server.js +92 -0
  83. package/dist/packages/cli/src/dash-cmd.js +113 -0
  84. package/dist/packages/cli/src/doctor.js +34 -0
  85. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  86. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  87. package/dist/packages/cli/src/live/signals.js +8 -0
  88. package/dist/packages/cli/src/main.js +98 -6
  89. package/dist/packages/cli/src/mcp-follow.js +21 -1
  90. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  91. package/dist/packages/cli/src/narratives.js +114 -0
  92. package/dist/packages/cli/src/overlay/model.js +147 -0
  93. package/dist/packages/cli/src/overlay/page.js +146 -0
  94. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  95. package/dist/packages/cli/src/overlay/server.js +170 -0
  96. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  97. package/dist/packages/cli/src/pnl-card.js +4 -0
  98. package/dist/packages/cli/src/record-cmd.js +82 -11
  99. package/dist/packages/cli/src/rules-cmd.js +2 -1
  100. package/dist/packages/cli/src/rules.js +24 -4
  101. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  102. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  103. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  104. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  105. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  106. package/dist/packages/cli/src/screens/narrative.js +154 -2
  107. package/dist/packages/cli/src/state.js +1 -0
  108. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  109. package/dist/packages/cli/src/version.js +1 -1
  110. package/dist/packages/custom/src/rules.js +5 -1
  111. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  112. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  113. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  114. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  115. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  116. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  117. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  118. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  119. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  120. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  121. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  122. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  123. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  124. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  127. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  131. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  132. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  140. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  158. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  160. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  162. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  163. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  164. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  165. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  174. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  175. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  176. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  177. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  179. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  180. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  181. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  207. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  209. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  210. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  211. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  212. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  213. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  214. package/dist/packages/mcp/src/index.js +1 -1
  215. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  216. package/dist/packages/mcp/src/server.js +125 -10
  217. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  218. package/dist/packages/narratives/src/cluster.js +193 -0
  219. package/dist/packages/narratives/src/embed.js +184 -0
  220. package/dist/packages/narratives/src/export.js +60 -0
  221. package/dist/packages/narratives/src/index.js +10 -0
  222. package/dist/packages/narratives/src/label.js +48 -0
  223. package/dist/packages/narratives/src/normalize.js +179 -0
  224. package/dist/packages/narratives/src/recorder.js +93 -0
  225. package/dist/packages/narratives/src/tracker.js +328 -0
  226. package/dist/packages/recorder/src/exit-probes.js +215 -0
  227. package/dist/packages/recorder/src/index.js +1 -0
  228. package/dist/packages/recorder/src/recorder.js +11 -1
  229. package/dist/packages/recorder/src/retention.js +4 -0
  230. package/dist/packages/recorder/src/schema.js +20 -1
  231. package/dist/packages/tui/src/keymap.js +12 -0
  232. package/package.json +3 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -0,0 +1,283 @@
1
+ // `dexterm autotrade forward start|status|stop|report` (0.2.0 card C). PAPER ONLY.
2
+ //
3
+ // A forward test is one strategy, frozen at its current version, on the LIVE tape, on paper, with its own bankroll and
4
+ // run file, for a window of hours or N closed trades (whichever comes first). `start` freezes it and starts its host
5
+ // detached (`forward run <fid>`, output in autotrade/forward/tests/<fid>.log); `--foreground` runs it in this terminal.
6
+ // It does not take the machine's run lock, so it runs next to the always-on service. At the end of the window the
7
+ // host flattens and writes the report: the forward numbers next to the backtest (held-out split) of the same rules.
8
+ // `--replay TAPE` is a smoke run on a recorded tape: labelled, never counted as the forward-tested rung.
9
+ //
10
+ // The analyst starts and stops the same tests (strategy_forward_start / _stop, never with a bankroll of its choosing);
11
+ // the Q screen's journey view shows them. Nothing here holds a key, signs or touches a wallet.
12
+ import { spawn } from 'node:child_process';
13
+ import { closeSync, openSync } from 'node:fs';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dextermHome, loadConfig } from '../../../custom/src/index.js';
16
+ import { pickBasis } from '../../../data/src/basis.js';
17
+ import { KillSwitch, createDataLayer, defaultDbPath, resolveRpcUrl } from '../../../data/src/index.js';
18
+ import { LiveSource, SessionGate, builtinTapePath, claudeSessionFactory, createForwardTest, effectiveCaps, findForwardTest, forwardLogPath, forwardReport, hostForward, liveState, listForwardTests, loadForwardTest, loadRunnerConfig, loadTape, markLost, prepareAccount, renderDossierRecord, requestForwardStop, runnerSystemPrompt, saveForwardTest, stubSession, validateStrategy, windowText, } from '../../../autotrade/src/index.js';
19
+ import { ollamaSessionFactory } from '../../../autotrade/src/ollama-sessions.js';
20
+ import { parseArgs, storeFor } from "./strategies.js";
21
+ import { larpVet, localEngine } from "./run.js";
22
+ const BOOL = ['json', 'stub', 'foreground', 'yes'];
23
+ export const FORWARD_USAGE = [
24
+ 'dexterm autotrade forward start <id> [--hours H] [--trades N] [--bankroll-usd N] [--foreground] [--stub] [--engine claude|ollama] [--model M] [--replay TAPE [--ms-per-tick N]]',
25
+ 'dexterm autotrade forward status [<id|fid>] [--json]',
26
+ 'dexterm autotrade forward stop <id|fid>',
27
+ 'dexterm autotrade forward report <id|fid> [--json]',
28
+ ];
29
+ const out = (io, s) => { io.stdout.write(`${s}\n`); };
30
+ const err = (io, s) => { io.stderr.write(`dexterm autotrade forward: ${s}\n`); };
31
+ const num = (v) => (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)) ? Number(v) : null);
32
+ /** The dexterm entry point a detached host runs (this package's bin). */
33
+ export const DEXTERM_BIN = fileURLToPath(new URL('../../bin/dexterm.mjs', import.meta.url));
34
+ /** Start the host of a `starting` test detached; its output goes to the test's log file. Returns the pid. */
35
+ export function spawnForwardHost(home, t, env, extra = [], bin = DEXTERM_BIN) {
36
+ const fd = openSync(forwardLogPath(home, t.forwardId), 'a', 0o600);
37
+ try {
38
+ const child = spawn(process.execPath, [bin, 'autotrade', 'forward', 'run', t.forwardId, ...extra], { detached: true, stdio: ['ignore', fd, fd], env: { ...env, DEXTERM_HOME: home } });
39
+ child.unref();
40
+ return child.pid ?? null;
41
+ }
42
+ finally {
43
+ closeSync(fd);
44
+ }
45
+ }
46
+ export async function forwardCmd(argv, io, deps = {}) {
47
+ const [sub, ...rest] = argv;
48
+ const home = dextermHome(io.env);
49
+ const now = deps.now ?? Date.now;
50
+ switch (sub) {
51
+ case 'start': return startCmd(rest, io, home, deps, now);
52
+ case 'run': return runHostCmd(rest, io, home, deps);
53
+ case 'status':
54
+ case undefined: return statusCmd(rest, io, home, now);
55
+ case 'stop': {
56
+ const ref = rest.find((a) => !a.startsWith('--'));
57
+ if (!ref) {
58
+ err(io, 'usage: dexterm autotrade forward stop <id|fid>');
59
+ return 2;
60
+ }
61
+ const t = findForwardTest(home, ref, now());
62
+ if (!t) {
63
+ err(io, `no forward test for ${JSON.stringify(ref)}`);
64
+ return 1;
65
+ }
66
+ const r = requestForwardStop(home, t.forwardId, 'cli', now());
67
+ if (!r.ok) {
68
+ err(io, r.why);
69
+ return 1;
70
+ }
71
+ out(io, `${r.already ? 'already asked to stop' : 'stop requested'}: ${t.forwardId} (${t.strategyId}) flattens at its next tick, writes its report and ends. A test stopped early is shown, not counted. Report: dexterm autotrade forward report ${t.forwardId}`);
72
+ return 0;
73
+ }
74
+ case 'report': return reportCmd(rest, io, home, now);
75
+ case 'help':
76
+ case '--help':
77
+ case '-h':
78
+ out(io, FORWARD_USAGE.join('\n'));
79
+ return 0;
80
+ default:
81
+ err(io, `unknown subcommand ${JSON.stringify(sub)}\n${FORWARD_USAGE.join('\n')}`);
82
+ return 2;
83
+ }
84
+ }
85
+ async function startCmd(argv, io, home, deps, now) {
86
+ const { pos, opts } = parseArgs(argv, BOOL);
87
+ const id = pos[0];
88
+ if (!id) {
89
+ err(io, `usage: ${FORWARD_USAGE[0]}`);
90
+ return 2;
91
+ }
92
+ if (opts.real || opts.live) {
93
+ err(io, 'a forward test is paper only (a real strategy forward-tests as its paper twin)');
94
+ return 2;
95
+ }
96
+ const store = storeFor(io);
97
+ const got = store.get(id);
98
+ if (!got) {
99
+ err(io, `no strategy ${JSON.stringify(id)} (dexterm autotrade list)`);
100
+ return 1;
101
+ }
102
+ if (!got.ok) {
103
+ err(io, `${id} does not validate: ${got.errors.map((e) => `${e.path} ${e.msg}`).join('; ')}`);
104
+ return 1;
105
+ }
106
+ const raw = store.raw(id);
107
+ const replay = typeof opts.replay === 'string' ? opts.replay : null;
108
+ const hours = num(opts.hours);
109
+ const trades = num(opts.trades);
110
+ if (opts.hours !== undefined && hours === null) {
111
+ err(io, '--hours needs a number');
112
+ return 2;
113
+ }
114
+ if (opts.trades !== undefined && trades === null) {
115
+ err(io, '--trades needs a whole number');
116
+ return 2;
117
+ }
118
+ const c = createForwardTest(home, got.strategy, raw, {
119
+ hours: hours ?? (trades === null ? 24 : null), trades, bankrollUsd: num(opts['bankroll-usd']), by: 'user', now: now(),
120
+ source: replay ? `replay ${replay}` : 'live', stub: !!opts.stub, msPerTick: num(opts['ms-per-tick']),
121
+ });
122
+ if (!c.ok) {
123
+ err(io, c.why);
124
+ return 1;
125
+ }
126
+ const t = c.test;
127
+ out(io, `forward test ${t.forwardId} · ${id} v${t.version} · PAPER · ${replay ? `replay ${replay} (a smoke run: it does not count as a forward test)` : 'live keyless data'} · own bankroll $${t.bankrollUsd} · window ${windowText(t.window)}`);
128
+ const extra = [...(opts.stub ? ['--stub'] : []), ...(typeof opts.engine === 'string' ? ['--engine', opts.engine] : []), ...(typeof opts.model === 'string' ? ['--model', opts.model] : [])];
129
+ if (opts.foreground)
130
+ return runHostCmd([t.forwardId, ...extra], io, home, deps);
131
+ const pid = (deps.spawnHost ?? ((h, x, e) => spawnForwardHost(h, x, io.env, e)))(home, t, extra);
132
+ // The host writes its own pid when it starts; only a test it has not picked up yet gets the spawn pid (liveness).
133
+ const cur = loadForwardTest(home, t.forwardId);
134
+ if (pid && cur && cur.state === 'starting' && cur.pid === null)
135
+ saveForwardTest(home, { ...cur, pid });
136
+ out(io, `started in the background${pid ? ` (pid ${pid})` : ''} · log ${forwardLogPath(home, t.forwardId)}`);
137
+ out(io, ` status: dexterm autotrade forward status ${id} · stop: dexterm autotrade forward stop ${id} · report: dexterm autotrade forward report ${id}`);
138
+ return 0;
139
+ }
140
+ /** The host (`forward run <fid>`): the live data layer (or a replay), sessions, the panic file; then hostForward. */
141
+ async function runHostCmd(argv, io, home, deps) {
142
+ const { pos, opts } = parseArgs(argv, BOOL);
143
+ const fid = pos[0];
144
+ const t = fid ? loadForwardTest(home, fid) : null;
145
+ if (!t) {
146
+ err(io, `no forward test ${JSON.stringify(fid ?? '')}`);
147
+ return 1;
148
+ }
149
+ if (t.state !== 'starting') {
150
+ err(io, `${t.forwardId} is ${t.state}; a forward test runs once (start a new one)`);
151
+ return 1;
152
+ }
153
+ const fail = (why) => { saveForwardTest(home, { ...t, state: 'failed', endedAt: Date.now(), endReason: why }); err(io, why); return 1; };
154
+ const kill = deps.killSwitch ?? new KillSwitch();
155
+ const panic = kill.engaged();
156
+ if (panic)
157
+ return fail(`the panic switch is engaged (${panic.reason}); nothing starts until \`dexterm wallet resume\``);
158
+ const v = validateStrategy(t.doc);
159
+ if (!v.ok)
160
+ return fail('the frozen strategy does not validate');
161
+ const strategy = v.strategy;
162
+ const cfg = loadRunnerConfig(home);
163
+ if (!cfg.ok)
164
+ return fail(`${cfg.path} is invalid: ${cfg.errors.join('; ')}`);
165
+ const basis = pickBasis(loadConfig(home).config.display?.priceMode, io.env);
166
+ const replay = t.source.startsWith('replay ') ? t.source.slice('replay '.length) : null;
167
+ const stub = t.stub || !!opts.stub;
168
+ const gate = new SessionGate(effectiveCaps(strategy, cfg.config), cfg.config.allowanceGuard);
169
+ const say = (s) => out(io, s);
170
+ let sessions;
171
+ if (stub)
172
+ sessions = stubSession();
173
+ else {
174
+ const eng = await localEngine(opts, io);
175
+ if ('error' in eng)
176
+ return fail(eng.error);
177
+ sessions = eng.engine === 'ollama'
178
+ ? ollamaSessionFactory({ client: eng.client, model: eng.model, numCtx: eng.numCtx, restriction: eng.level, live: false, systemPrompt: (s) => runnerSystemPrompt(s, { live: !replay, basis }), gate, log: () => { } })
179
+ : claudeSessionFactory({ bin: io.env.DEXTERM_CLAUDE_BIN || 'claude', account: () => prepareAccount(cfg.config.account, io.env), model: typeof opts.model === 'string' ? opts.model : undefined, systemPrompt: (s) => runnerSystemPrompt(s, { live: !replay, basis }), gate, log: () => { } });
180
+ }
181
+ if (replay) {
182
+ let tape;
183
+ try {
184
+ tape = loadTape(builtinTapePath(replay));
185
+ }
186
+ catch (e) {
187
+ return fail(e.message);
188
+ }
189
+ const r = await hostForward({ home, forwardId: t.forwardId, tape, sessions, gate, pacing: { mode: 'paced', msPerTick: t.msPerTick ?? 200 }, killSwitch: kill, onLine: say });
190
+ if (!r.ok) {
191
+ err(io, r.why);
192
+ return 1;
193
+ }
194
+ for (const l of forwardReport(home, r.test).lines)
195
+ say(l);
196
+ return 0;
197
+ }
198
+ const rpc = resolveRpcUrl({ env: io.env });
199
+ const data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env), rpcUrl: rpc.url, rpcFallbackUrl: rpc.fallbackUrl });
200
+ const live = new LiveSource({ store: data.store, dexscreener: data.dexscreener }, { onLog: () => { } });
201
+ const { McpTools } = await import('../../../mcp/src/index.js');
202
+ const mcp = new McpTools(data, () => ({ screen: 'autotrade-forward', actions: { mints: [], feeds: [], sortKeys: [] } }));
203
+ const dossier = async (k, h, matched, b) => {
204
+ let rec;
205
+ try {
206
+ rec = await mcp.call('get_dossier', { mint: k.mint, interval: '5m' });
207
+ }
208
+ catch (e) {
209
+ rec = { availability: { present: [], missing: [`dossier: ${e.message}`], knownUnavailable: [] } };
210
+ }
211
+ return renderDossierRecord(rec, { mint: k.mint, symbol: k.symbol, matched }, k, h, b, basis);
212
+ };
213
+ const larp = strategy.entry.safety.larp;
214
+ const vet = larp ? larpVet(larp, (mint) => mcp.call('larp_check', { mint })) : undefined;
215
+ const onSig = () => { requestForwardStop(home, t.forwardId, 'signal'); };
216
+ process.once('SIGINT', onSig);
217
+ process.once('SIGTERM', onSig);
218
+ try {
219
+ const r = await hostForward({
220
+ home, forwardId: t.forwardId, source: { name: 'live keyless data layer', ticks: live.ticks(), startT: Date.now(), stop: () => live.stop() },
221
+ sessions, gate, dossier, vet, pacing: { mode: 'live' }, killSwitch: kill, onLine: say,
222
+ });
223
+ if (!r.ok) {
224
+ err(io, r.why);
225
+ return 1;
226
+ }
227
+ for (const l of forwardReport(home, r.test).lines)
228
+ say(l);
229
+ return r.test.state === 'failed' ? 1 : 0;
230
+ }
231
+ finally {
232
+ process.removeListener('SIGINT', onSig);
233
+ process.removeListener('SIGTERM', onSig);
234
+ live.stop();
235
+ data.close();
236
+ }
237
+ }
238
+ function statusCmd(argv, io, home, now) {
239
+ const { pos, opts } = parseArgs(argv, BOOL);
240
+ const ref = pos[0];
241
+ const tests = ref ? [findForwardTest(home, ref, now())].filter((x) => !!x) : listForwardTests(home).slice(0, 20);
242
+ if (ref && !tests.length) {
243
+ err(io, `no forward test for ${JSON.stringify(ref)}`);
244
+ return 1;
245
+ }
246
+ const rows = tests.map((t) => ({ forwardId: t.forwardId, strategy: t.strategyId, version: t.version, state: liveState(t, now()), source: t.source, window: windowText(t.window), startedAt: t.startedAt, endsAt: t.endsAt, progress: t.progress, endReason: t.endReason }));
247
+ if (opts.json) {
248
+ out(io, JSON.stringify({ paper: true, tests: rows }, null, 2));
249
+ return 0;
250
+ }
251
+ if (!rows.length) {
252
+ out(io, 'no forward tests yet (dexterm autotrade forward start <id> --hours 24)');
253
+ return 0;
254
+ }
255
+ for (const r of rows) {
256
+ const p = r.progress;
257
+ out(io, `${r.forwardId} ${r.strategy.padEnd(18)} v${r.version} ${r.state.toUpperCase().padEnd(8)} ${r.window}${p ? ` · ${p.closed} closed, ${p.open} open, equity $${p.equityUsd.toFixed(2)}` : ''}${r.endReason ? ` · ${r.endReason}` : ''}${r.source === 'live' ? '' : ` · ${r.source}`}`);
258
+ }
259
+ return 0;
260
+ }
261
+ function reportCmd(argv, io, home, now) {
262
+ const { pos, opts } = parseArgs(argv, BOOL);
263
+ const ref = pos[0];
264
+ if (!ref) {
265
+ err(io, 'usage: dexterm autotrade forward report <id|fid> [--json]');
266
+ return 2;
267
+ }
268
+ let t = findForwardTest(home, ref, now());
269
+ if (!t) {
270
+ err(io, `no forward test for ${JSON.stringify(ref)}`);
271
+ return 1;
272
+ }
273
+ if (liveState(t, now()) === 'lost')
274
+ t = markLost(home, t, now());
275
+ const r = forwardReport(home, t, now());
276
+ if (opts.json) {
277
+ out(io, JSON.stringify({ paper: true, ...r }, null, 2));
278
+ return 0;
279
+ }
280
+ for (const l of r.lines)
281
+ out(io, l);
282
+ return 0;
283
+ }
@@ -0,0 +1,208 @@
1
+ // `dexterm autotrade journey` and `dexterm autotrade promote` (0.2.0 card C): a strategy's ladder, and the user's side
2
+ // of a promotion.
3
+ //
4
+ // draft -> backtested -> forward-tested -> always-on (paper) -> real
5
+ //
6
+ // `journey [<id>]` shows where each strategy stands and what the next rung needs. `promote <id> --to always-on|real`
7
+ // writes a promotion proposal (the analyst writes the same file with strategy_promote_propose) and shows its evidence
8
+ // card; `promote list|show|apply|dismiss` resolve them. Applying always-on needs `y` typed on a real terminal (never a
9
+ // pipe, a flag or an agent) and adds the strategy to autotrade.json as a paper member of the always-on service.
10
+ // Real is never applied here: it hands over to `dexterm autotrade signal arm` (the evidence panel, the user's own
11
+ // caps, a typed phrase with a one-time code), and refuses until the user has set the real-mode caps themselves.
12
+ // No rung can be skipped on this path. task-1679: real also needs the evidence floor (a completed forward test with
13
+ // the user's minForwardTrades closed paper trades + a backtest); below it `promote --to real` refuses and says how the
14
+ // user alone can override it (`signal arm`, a typed OVERRIDE phrase on a real terminal, journaled).
15
+ import { dextermHome } from '../../../custom/src/index.js';
16
+ import { PromotionFiles, applyAlwaysOn, checkPromotion, dismissPromotion, findForwardTest, isActive, isPromotionId, journeyView, loadArmings, loadCaps, proposePromotion, renderJourney, windowText, } from '../../../autotrade/src/index.js';
17
+ import { parseArgs, storeFor } from "./strategies.js";
18
+ import { signalCmd } from "./signal.js";
19
+ const BOOL = ['json'];
20
+ export const JOURNEY_USAGE = 'dexterm autotrade journey [<id>] [--json]';
21
+ export const PROMOTE_USAGE = [
22
+ 'dexterm autotrade promote <id> --to always-on|real',
23
+ 'dexterm autotrade promote list [--json] | show <pp-id> | apply <pp-id> | dismiss <pp-id>',
24
+ ].join('\n');
25
+ const out = (io, s) => { io.stdout.write(`${s}\n`); };
26
+ const err = (io, cmd, s) => { io.stderr.write(`dexterm autotrade ${cmd}: ${s}\n`); };
27
+ function viewOf(io, id) {
28
+ const home = dextermHome(io.env);
29
+ const got = storeFor(io).get(id);
30
+ if (!got)
31
+ return { error: `no strategy ${JSON.stringify(id)} (dexterm autotrade list)` };
32
+ if (!got.ok)
33
+ return { error: `${id} does not validate: ${got.errors.map((e) => `${e.path} ${e.msg}`).join('; ')}` };
34
+ return { s: got.strategy, v: journeyView(home, got.strategy, { armedHash: loadArmings(home)[id]?.strategyHash ?? null }) };
35
+ }
36
+ export function journeyCmd(argv, io) {
37
+ const { pos, opts } = parseArgs(argv, BOOL);
38
+ const home = dextermHome(io.env);
39
+ const store = storeFor(io);
40
+ if (!pos[0]) {
41
+ const rows = store.list().filter((e) => e.source === 'user' && e.ok).map((e) => { const r = viewOf(io, e.id); return 'error' in r ? null : r.v; }).filter((v) => !!v);
42
+ if (opts.json) {
43
+ out(io, JSON.stringify({ paper: true, strategies: rows.map((v) => ({ id: v.id, version: v.version, stage: v.stage, next: v.next?.stage ?? null })) }, null, 2));
44
+ return 0;
45
+ }
46
+ if (!rows.length) {
47
+ out(io, 'no strategies of your own yet: copy a preset (dexterm autotrade new my-scalp --from scalp) or ask the analyst to build one');
48
+ return 0;
49
+ }
50
+ for (const v of rows)
51
+ out(io, `${v.id.padEnd(20)} v${v.version.padEnd(8)} ${v.stage.toUpperCase().padEnd(15)} ${v.next ? `next: ${v.next.stage}` : 'top of the ladder'}`);
52
+ return 0;
53
+ }
54
+ const r = viewOf(io, pos[0]);
55
+ if ('error' in r) {
56
+ err(io, 'journey', r.error);
57
+ return 1;
58
+ }
59
+ const fw = findForwardTest(home, r.s.id);
60
+ const running = fw && isActive(fw) ? fw : null;
61
+ const pending = new PromotionFiles(home).list({ status: 'pending', id: r.s.id });
62
+ if (opts.json) {
63
+ out(io, JSON.stringify({ paper: r.s.mode !== 'real', ...r.v, forwardRunning: running ? { forwardId: running.forwardId, window: running.window, progress: running.progress } : null, pendingPromotions: pending.map((p) => ({ proposalId: p.proposalId, to: p.to, actor: p.actor })) }, null, 2));
64
+ return 0;
65
+ }
66
+ for (const l of renderJourney(r.v))
67
+ out(io, l);
68
+ if (running)
69
+ out(io, ` forward test running: ${running.forwardId} · window ${windowText(running.window)}${running.progress ? ` · ${running.progress.closed} closed, ${running.progress.open} open` : ''}`);
70
+ for (const p of pending)
71
+ out(io, ` pending promotion ${p.proposalId} → ${p.to} (proposed by ${p.actor}): dexterm autotrade promote show ${p.proposalId}`);
72
+ return 0;
73
+ }
74
+ function isTty(io, deps) { return deps.tty ?? io.stdin?.isTTY === true; }
75
+ async function typedLine(io, deps, prompt) {
76
+ if (deps.typed)
77
+ return deps.typed(prompt);
78
+ io.stderr.write(prompt);
79
+ const stdin = io.stdin;
80
+ if (!stdin)
81
+ return null;
82
+ const { createInterface } = await import('node:readline');
83
+ const rl = createInterface({ input: stdin, terminal: false });
84
+ try {
85
+ return await new Promise((resolve) => { rl.once('line', (l) => resolve(l)); rl.once('close', () => resolve(null)); });
86
+ }
87
+ finally {
88
+ rl.close();
89
+ stdin.pause?.();
90
+ }
91
+ }
92
+ function showCard(io, p) {
93
+ out(io, `promotion ${p.proposalId} · ${p.status.toUpperCase()} · proposed by ${p.actor} · ${new Date(p.createdAt).toISOString().slice(0, 16).replace('T', ' ')}`);
94
+ out(io, ` why (as given, display only): ${p.why}`);
95
+ for (const l of p.card)
96
+ out(io, l);
97
+ }
98
+ export async function promoteCmd(argv, io, deps = {}) {
99
+ const { pos, opts } = parseArgs(argv, BOOL);
100
+ const home = dextermHome(io.env);
101
+ const files = new PromotionFiles(home);
102
+ const now = deps.now ?? Date.now;
103
+ const sub = pos[0];
104
+ if (!sub || sub === 'help') {
105
+ out(io, PROMOTE_USAGE);
106
+ return sub ? 0 : 2;
107
+ }
108
+ if (sub === 'list') {
109
+ const all = files.list().slice(0, 30);
110
+ if (opts.json) {
111
+ out(io, JSON.stringify({ promotions: all.map(({ card: _c, ...p }) => p) }, null, 2));
112
+ return 0;
113
+ }
114
+ if (!all.length) {
115
+ out(io, 'no promotion proposals');
116
+ return 0;
117
+ }
118
+ for (const p of all)
119
+ out(io, `${p.proposalId} ${p.id.padEnd(20)} → ${p.to.padEnd(9)} ${p.status.padEnd(9)} by ${p.actor}`);
120
+ return 0;
121
+ }
122
+ if (sub === 'show' || sub === 'apply' || sub === 'dismiss') {
123
+ const pid = pos[1];
124
+ const p = isPromotionId(pid) ? files.get(pid) : null;
125
+ if (!p) {
126
+ err(io, 'promote', `no promotion ${JSON.stringify(pid ?? '')} (dexterm autotrade promote list)`);
127
+ return 1;
128
+ }
129
+ if (sub === 'show') {
130
+ showCard(io, p);
131
+ return 0;
132
+ }
133
+ if (p.status !== 'pending') {
134
+ err(io, 'promote', `${p.proposalId} is ${p.status}`);
135
+ return 1;
136
+ }
137
+ if (sub === 'dismiss') {
138
+ dismissPromotion(home, p.proposalId, 'user', now());
139
+ out(io, `dismissed ${p.proposalId}; nothing changed`);
140
+ return 0;
141
+ }
142
+ return applyCmd(io, deps, p, now);
143
+ }
144
+ // promote <id> --to always-on|real: the user's own proposal, then straight to the confirm.
145
+ const id = sub;
146
+ const to = typeof opts.to === 'string' ? opts.to : '';
147
+ const r = viewOf(io, id);
148
+ if ('error' in r) {
149
+ err(io, 'promote', r.error);
150
+ return 1;
151
+ }
152
+ const armedHash = loadArmings(home)[id]?.strategyHash ?? null;
153
+ const c = checkPromotion(home, r.s, to, { armedHash });
154
+ if (!c.ok) {
155
+ err(io, 'promote', c.why);
156
+ if (c.view)
157
+ for (const l of renderJourney(c.view))
158
+ io.stderr.write(`${l}\n`);
159
+ if (c.reason === 'evidence-floor')
160
+ io.stderr.write(` the floor is your limit minForwardTrades (dexterm autotrade limits explain minForwardTrades). To arm below it anyway, your call alone: dexterm autotrade signal arm ${id} on your own terminal asks for a typed OVERRIDE phrase, which is journaled.\n`);
161
+ return 1;
162
+ }
163
+ const made = proposePromotion(home, r.s, to, { actor: 'user', why: 'asked on the command line', now: now(), armedHash });
164
+ if (!made.ok) {
165
+ err(io, 'promote', made.why);
166
+ return 1;
167
+ }
168
+ return applyCmd(io, deps, made.proposal, now);
169
+ }
170
+ async function applyCmd(io, deps, p, now) {
171
+ const home = dextermHome(io.env);
172
+ const r = viewOf(io, p.id);
173
+ if ('error' in r) {
174
+ err(io, 'promote', r.error);
175
+ return 1;
176
+ }
177
+ if (r.s.hash !== p.hash) {
178
+ err(io, 'promote', `${p.id} changed since the proposal (v${p.version} → v${r.s.version}); dismiss it and ask for a new one`);
179
+ return 1;
180
+ }
181
+ showCard(io, p);
182
+ if (p.to === 'real') {
183
+ // Caps are the user's: real is only reachable here once the user set them on purpose (task-1655: card E's limits.json).
184
+ const caps = loadCaps(home);
185
+ if (!caps.ok) {
186
+ err(io, 'promote', `set your own real-mode caps first (your limits: dexterm autotrade limits set perTradeSol=X dailySol=Y buysPerDay=N maxOpenPositions=N, or signal caps set); ${caps.why}`);
187
+ return 1;
188
+ }
189
+ out(io, `real mode is armed by card A's typed confirmation; handing over to: dexterm autotrade signal arm ${p.id}`);
190
+ return signalCmd(['arm', p.id], io, deps);
191
+ }
192
+ if (!isTty(io, deps)) {
193
+ err(io, 'promote', 'applying a promotion needs your y on a real terminal (a pipe, a flag or an agent cannot give it); or apply it on the Q screen');
194
+ return 1;
195
+ }
196
+ const got = await typedLine(io, deps, `add ${p.id} to the always-on service (paper)? type y to apply, anything else cancels: `);
197
+ if ((got ?? '').trim().toLowerCase() !== 'y') {
198
+ err(io, 'promote', 'not applied; nothing changed (the proposal stays pending)');
199
+ return 1;
200
+ }
201
+ const a = applyAlwaysOn(home, p.proposalId, r.s, { by: 'user', now: now() });
202
+ if (!a.ok) {
203
+ err(io, 'promote', a.why);
204
+ return 1;
205
+ }
206
+ out(io, `applied: ${p.id} is ${a.created ? 'now' : 'again'} an always-on paper member (${a.memberId}) in autotrade.json. The service runs it from its next start: dexterm autotrade service status (restart it to pick it up now: service stop, then service start)`);
207
+ return 0;
208
+ }