magi-ai 0.1.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 (300) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +377 -0
  3. package/README.md +377 -0
  4. package/dist/bin/magi-benchmark.d.ts +14 -0
  5. package/dist/bin/magi-benchmark.js +93 -0
  6. package/dist/bin/magi-mcp.d.ts +8 -0
  7. package/dist/bin/magi-mcp.js +28 -0
  8. package/dist/bin/magi.d.ts +2 -0
  9. package/dist/bin/magi.js +634 -0
  10. package/dist/src/adapters/base.d.ts +34 -0
  11. package/dist/src/adapters/base.js +149 -0
  12. package/dist/src/adapters/claude.d.ts +29 -0
  13. package/dist/src/adapters/claude.js +65 -0
  14. package/dist/src/adapters/codex.d.ts +21 -0
  15. package/dist/src/adapters/codex.js +41 -0
  16. package/dist/src/adapters/gemini.d.ts +18 -0
  17. package/dist/src/adapters/gemini.js +31 -0
  18. package/dist/src/adapters/registry.d.ts +19 -0
  19. package/dist/src/adapters/registry.js +59 -0
  20. package/dist/src/audit/hash-chain.d.ts +21 -0
  21. package/dist/src/audit/hash-chain.js +70 -0
  22. package/dist/src/audit/types.d.ts +25 -0
  23. package/dist/src/audit/types.js +1 -0
  24. package/dist/src/audit/writer.d.ts +18 -0
  25. package/dist/src/audit/writer.js +100 -0
  26. package/dist/src/benchmark/golden-tasks.d.ts +9 -0
  27. package/dist/src/benchmark/golden-tasks.js +476 -0
  28. package/dist/src/benchmark/reporter.d.ts +5 -0
  29. package/dist/src/benchmark/reporter.js +107 -0
  30. package/dist/src/benchmark/runner.d.ts +30 -0
  31. package/dist/src/benchmark/runner.js +224 -0
  32. package/dist/src/benchmark/scorer.d.ts +12 -0
  33. package/dist/src/benchmark/scorer.js +124 -0
  34. package/dist/src/benchmark/types.d.ts +54 -0
  35. package/dist/src/benchmark/types.js +1 -0
  36. package/dist/src/cache/deliberation-cache.d.ts +49 -0
  37. package/dist/src/cache/deliberation-cache.js +127 -0
  38. package/dist/src/cli/commands/config-cmd.d.ts +11 -0
  39. package/dist/src/cli/commands/config-cmd.js +190 -0
  40. package/dist/src/cli/commands/demo.d.ts +12 -0
  41. package/dist/src/cli/commands/demo.js +66 -0
  42. package/dist/src/cli/commands/setup.d.ts +7 -0
  43. package/dist/src/cli/commands/setup.js +182 -0
  44. package/dist/src/cli/i18n.d.ts +89 -0
  45. package/dist/src/cli/i18n.js +176 -0
  46. package/dist/src/cli/interactive-select.d.ts +27 -0
  47. package/dist/src/cli/interactive-select.js +130 -0
  48. package/dist/src/cli/tui-setup.d.ts +24 -0
  49. package/dist/src/cli/tui-setup.js +42 -0
  50. package/dist/src/config/cli-detector.d.ts +37 -0
  51. package/dist/src/config/cli-detector.js +99 -0
  52. package/dist/src/config/user-config.d.ts +81 -0
  53. package/dist/src/config/user-config.js +134 -0
  54. package/dist/src/context/auto-collector.d.ts +43 -0
  55. package/dist/src/context/auto-collector.js +337 -0
  56. package/dist/src/context/manager.d.ts +35 -0
  57. package/dist/src/context/manager.js +162 -0
  58. package/dist/src/context/serializer.d.ts +20 -0
  59. package/dist/src/context/serializer.js +52 -0
  60. package/dist/src/demo/recorded-deliberation.d.ts +13 -0
  61. package/dist/src/demo/recorded-deliberation.js +277 -0
  62. package/dist/src/engine/angel-detector.d.ts +83 -0
  63. package/dist/src/engine/angel-detector.js +334 -0
  64. package/dist/src/engine/at-field.d.ts +40 -0
  65. package/dist/src/engine/at-field.js +195 -0
  66. package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
  67. package/dist/src/engine/berserk-orchestrator.js +378 -0
  68. package/dist/src/engine/change-metrics.d.ts +56 -0
  69. package/dist/src/engine/change-metrics.js +214 -0
  70. package/dist/src/engine/consensus.d.ts +20 -0
  71. package/dist/src/engine/consensus.js +146 -0
  72. package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
  73. package/dist/src/engine/dead-sea-scrolls.js +610 -0
  74. package/dist/src/engine/drift-detector.d.ts +39 -0
  75. package/dist/src/engine/drift-detector.js +225 -0
  76. package/dist/src/engine/dummy-plug.d.ts +44 -0
  77. package/dist/src/engine/dummy-plug.js +190 -0
  78. package/dist/src/engine/engram-manager.d.ts +55 -0
  79. package/dist/src/engine/engram-manager.js +306 -0
  80. package/dist/src/engine/events.d.ts +130 -0
  81. package/dist/src/engine/events.js +44 -0
  82. package/dist/src/engine/gospel.d.ts +30 -0
  83. package/dist/src/engine/gospel.js +129 -0
  84. package/dist/src/engine/hallucination-detector.d.ts +33 -0
  85. package/dist/src/engine/hallucination-detector.js +215 -0
  86. package/dist/src/engine/human-resolver.d.ts +19 -0
  87. package/dist/src/engine/human-resolver.js +89 -0
  88. package/dist/src/engine/instrumentality.d.ts +64 -0
  89. package/dist/src/engine/instrumentality.js +297 -0
  90. package/dist/src/engine/iruel-battle.d.ts +79 -0
  91. package/dist/src/engine/iruel-battle.js +319 -0
  92. package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
  93. package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
  94. package/dist/src/engine/kernel/index.d.ts +8 -0
  95. package/dist/src/engine/kernel/index.js +7 -0
  96. package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
  97. package/dist/src/engine/kernel/phase-runner.js +155 -0
  98. package/dist/src/engine/kernel/post-processor.d.ts +17 -0
  99. package/dist/src/engine/kernel/post-processor.js +131 -0
  100. package/dist/src/engine/kernel/types.d.ts +107 -0
  101. package/dist/src/engine/kernel/types.js +1 -0
  102. package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
  103. package/dist/src/engine/kernel/unit-executor.js +132 -0
  104. package/dist/src/engine/lcl-manager.d.ts +44 -0
  105. package/dist/src/engine/lcl-manager.js +143 -0
  106. package/dist/src/engine/middleware/cache.d.ts +7 -0
  107. package/dist/src/engine/middleware/cache.js +29 -0
  108. package/dist/src/engine/middleware/chain.d.ts +18 -0
  109. package/dist/src/engine/middleware/chain.js +45 -0
  110. package/dist/src/engine/middleware/firewall.d.ts +8 -0
  111. package/dist/src/engine/middleware/firewall.js +24 -0
  112. package/dist/src/engine/middleware/index.d.ts +4 -0
  113. package/dist/src/engine/middleware/index.js +3 -0
  114. package/dist/src/engine/middleware/types.d.ts +43 -0
  115. package/dist/src/engine/middleware/types.js +1 -0
  116. package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
  117. package/dist/src/engine/nebuchadnezzar-key.js +203 -0
  118. package/dist/src/engine/neon-genesis.d.ts +52 -0
  119. package/dist/src/engine/neon-genesis.js +203 -0
  120. package/dist/src/engine/objective-judge.d.ts +53 -0
  121. package/dist/src/engine/objective-judge.js +214 -0
  122. package/dist/src/engine/offline-mode.d.ts +18 -0
  123. package/dist/src/engine/offline-mode.js +46 -0
  124. package/dist/src/engine/orchestrator.d.ts +79 -0
  125. package/dist/src/engine/orchestrator.js +58 -0
  126. package/dist/src/engine/secret-cipher.d.ts +26 -0
  127. package/dist/src/engine/secret-cipher.js +114 -0
  128. package/dist/src/engine/seele-council.d.ts +90 -0
  129. package/dist/src/engine/seele-council.js +482 -0
  130. package/dist/src/engine/self-destruct.d.ts +61 -0
  131. package/dist/src/engine/self-destruct.js +231 -0
  132. package/dist/src/engine/self-evolution.d.ts +64 -0
  133. package/dist/src/engine/self-evolution.js +368 -0
  134. package/dist/src/engine/sync-rate.d.ts +45 -0
  135. package/dist/src/engine/sync-rate.js +151 -0
  136. package/dist/src/engine/type666-firewall.d.ts +76 -0
  137. package/dist/src/engine/type666-firewall.js +343 -0
  138. package/dist/src/engine/umbilical-cable.d.ts +41 -0
  139. package/dist/src/engine/umbilical-cable.js +192 -0
  140. package/dist/src/index.d.ts +106 -0
  141. package/dist/src/index.js +426 -0
  142. package/dist/src/mcp/server.d.ts +38 -0
  143. package/dist/src/mcp/server.js +196 -0
  144. package/dist/src/metrics/token-tracker.d.ts +38 -0
  145. package/dist/src/metrics/token-tracker.js +112 -0
  146. package/dist/src/parsers/json-extractor.d.ts +9 -0
  147. package/dist/src/parsers/json-extractor.js +239 -0
  148. package/dist/src/parsers/opinion-schema.d.ts +81 -0
  149. package/dist/src/parsers/opinion-schema.js +147 -0
  150. package/dist/src/parsers/unstructured-parser.d.ts +20 -0
  151. package/dist/src/parsers/unstructured-parser.js +122 -0
  152. package/dist/src/pipelines/architecture.d.ts +10 -0
  153. package/dist/src/pipelines/architecture.js +9 -0
  154. package/dist/src/pipelines/bug-analysis.d.ts +9 -0
  155. package/dist/src/pipelines/bug-analysis.js +8 -0
  156. package/dist/src/pipelines/code-review.d.ts +10 -0
  157. package/dist/src/pipelines/code-review.js +30 -0
  158. package/dist/src/pipelines/custom.d.ts +14 -0
  159. package/dist/src/pipelines/custom.js +29 -0
  160. package/dist/src/pipelines/registry.d.ts +9 -0
  161. package/dist/src/pipelines/registry.js +20 -0
  162. package/dist/src/prompts/personas.d.ts +6 -0
  163. package/dist/src/prompts/personas.js +44 -0
  164. package/dist/src/prompts/schemas.d.ts +4 -0
  165. package/dist/src/prompts/schemas.js +24 -0
  166. package/dist/src/prompts/templates.d.ts +6 -0
  167. package/dist/src/prompts/templates.js +91 -0
  168. package/dist/src/repl/accessibility.d.ts +23 -0
  169. package/dist/src/repl/accessibility.js +46 -0
  170. package/dist/src/repl/banner.d.ts +4 -0
  171. package/dist/src/repl/banner.js +28 -0
  172. package/dist/src/repl/boot-animation.d.ts +13 -0
  173. package/dist/src/repl/boot-animation.js +143 -0
  174. package/dist/src/repl/completer.d.ts +21 -0
  175. package/dist/src/repl/completer.js +168 -0
  176. package/dist/src/repl/context.d.ts +24 -0
  177. package/dist/src/repl/context.js +42 -0
  178. package/dist/src/repl/display-utils.d.ts +13 -0
  179. package/dist/src/repl/display-utils.js +65 -0
  180. package/dist/src/repl/event-listener.d.ts +18 -0
  181. package/dist/src/repl/event-listener.js +112 -0
  182. package/dist/src/repl/export-formatter.d.ts +8 -0
  183. package/dist/src/repl/export-formatter.js +73 -0
  184. package/dist/src/repl/ghost-text.d.ts +31 -0
  185. package/dist/src/repl/ghost-text.js +119 -0
  186. package/dist/src/repl/handoff-animation.d.ts +15 -0
  187. package/dist/src/repl/handoff-animation.js +65 -0
  188. package/dist/src/repl/history.d.ts +16 -0
  189. package/dist/src/repl/history.js +130 -0
  190. package/dist/src/repl/job-registry.d.ts +26 -0
  191. package/dist/src/repl/job-registry.js +80 -0
  192. package/dist/src/repl/magi-repl.d.ts +72 -0
  193. package/dist/src/repl/magi-repl.js +1008 -0
  194. package/dist/src/repl/multiline-input.d.ts +45 -0
  195. package/dist/src/repl/multiline-input.js +78 -0
  196. package/dist/src/repl/prompt-builder.d.ts +19 -0
  197. package/dist/src/repl/prompt-builder.js +36 -0
  198. package/dist/src/repl/repl-state.d.ts +5 -0
  199. package/dist/src/repl/repl-state.js +19 -0
  200. package/dist/src/repl/result-display.d.ts +8 -0
  201. package/dist/src/repl/result-display.js +195 -0
  202. package/dist/src/repl/session-stats.d.ts +26 -0
  203. package/dist/src/repl/session-stats.js +119 -0
  204. package/dist/src/repl/slash-commands.d.ts +60 -0
  205. package/dist/src/repl/slash-commands.js +725 -0
  206. package/dist/src/repl/terminal-sanitize.d.ts +14 -0
  207. package/dist/src/repl/terminal-sanitize.js +19 -0
  208. package/dist/src/reporters/console.d.ts +7 -0
  209. package/dist/src/reporters/console.js +78 -0
  210. package/dist/src/reporters/json.d.ts +2 -0
  211. package/dist/src/reporters/json.js +3 -0
  212. package/dist/src/reporters/markdown.d.ts +2 -0
  213. package/dist/src/reporters/markdown.js +65 -0
  214. package/dist/src/reporters/streaming.d.ts +20 -0
  215. package/dist/src/reporters/streaming.js +178 -0
  216. package/dist/src/tui/activity-log.d.ts +23 -0
  217. package/dist/src/tui/activity-log.js +67 -0
  218. package/dist/src/tui/animations.d.ts +39 -0
  219. package/dist/src/tui/animations.js +167 -0
  220. package/dist/src/tui/ansi.d.ts +28 -0
  221. package/dist/src/tui/ansi.js +51 -0
  222. package/dist/src/tui/boot-sequence.d.ts +11 -0
  223. package/dist/src/tui/boot-sequence.js +98 -0
  224. package/dist/src/tui/colors.d.ts +101 -0
  225. package/dist/src/tui/colors.js +71 -0
  226. package/dist/src/tui/header.d.ts +24 -0
  227. package/dist/src/tui/header.js +122 -0
  228. package/dist/src/tui/index.d.ts +3 -0
  229. package/dist/src/tui/index.js +3 -0
  230. package/dist/src/tui/keypress.d.ts +25 -0
  231. package/dist/src/tui/keypress.js +95 -0
  232. package/dist/src/tui/layout.d.ts +74 -0
  233. package/dist/src/tui/layout.js +171 -0
  234. package/dist/src/tui/magi-tui.d.ts +101 -0
  235. package/dist/src/tui/magi-tui.js +754 -0
  236. package/dist/src/tui/panel.d.ts +45 -0
  237. package/dist/src/tui/panel.js +292 -0
  238. package/dist/src/tui/screen-buffer.d.ts +54 -0
  239. package/dist/src/tui/screen-buffer.js +262 -0
  240. package/dist/src/tui/status-bar.d.ts +25 -0
  241. package/dist/src/tui/status-bar.js +124 -0
  242. package/dist/src/tui/terminal-detect.d.ts +26 -0
  243. package/dist/src/tui/terminal-detect.js +44 -0
  244. package/dist/src/tui/tui-helpers.d.ts +12 -0
  245. package/dist/src/tui/tui-helpers.js +37 -0
  246. package/dist/src/types/adapter.d.ts +75 -0
  247. package/dist/src/types/adapter.js +36 -0
  248. package/dist/src/types/config.d.ts +108 -0
  249. package/dist/src/types/config.js +85 -0
  250. package/dist/src/types/consensus.d.ts +55 -0
  251. package/dist/src/types/consensus.js +17 -0
  252. package/dist/src/types/core.d.ts +178 -0
  253. package/dist/src/types/core.js +85 -0
  254. package/dist/src/types/magi-api.d.ts +62 -0
  255. package/dist/src/types/magi-api.js +7 -0
  256. package/dist/src/types/phase-h.d.ts +142 -0
  257. package/dist/src/types/phase-h.js +7 -0
  258. package/dist/src/types/phase-i.d.ts +186 -0
  259. package/dist/src/types/phase-i.js +6 -0
  260. package/dist/src/types/phase-k.d.ts +259 -0
  261. package/dist/src/types/phase-k.js +6 -0
  262. package/dist/src/types/phase-l.d.ts +199 -0
  263. package/dist/src/types/phase-l.js +6 -0
  264. package/dist/src/types/pipeline.d.ts +37 -0
  265. package/dist/src/types/pipeline.js +2 -0
  266. package/dist/src/utils/abstain-factory.d.ts +2 -0
  267. package/dist/src/utils/abstain-factory.js +18 -0
  268. package/dist/src/utils/errors.d.ts +34 -0
  269. package/dist/src/utils/errors.js +59 -0
  270. package/dist/src/utils/file-validator.d.ts +50 -0
  271. package/dist/src/utils/file-validator.js +124 -0
  272. package/dist/src/utils/fire-and-forget.d.ts +5 -0
  273. package/dist/src/utils/fire-and-forget.js +10 -0
  274. package/dist/src/utils/flag-validator.d.ts +21 -0
  275. package/dist/src/utils/flag-validator.js +79 -0
  276. package/dist/src/utils/freeze.d.ts +8 -0
  277. package/dist/src/utils/freeze.js +16 -0
  278. package/dist/src/utils/language-detector.d.ts +16 -0
  279. package/dist/src/utils/language-detector.js +159 -0
  280. package/dist/src/utils/latency-tracker.d.ts +45 -0
  281. package/dist/src/utils/latency-tracker.js +100 -0
  282. package/dist/src/utils/logger.d.ts +33 -0
  283. package/dist/src/utils/logger.js +112 -0
  284. package/dist/src/utils/process.d.ts +40 -0
  285. package/dist/src/utils/process.js +253 -0
  286. package/dist/src/utils/retry.d.ts +12 -0
  287. package/dist/src/utils/retry.js +30 -0
  288. package/dist/src/utils/safe-fs.d.ts +38 -0
  289. package/dist/src/utils/safe-fs.js +56 -0
  290. package/dist/src/utils/safe-json-parse.d.ts +15 -0
  291. package/dist/src/utils/safe-json-parse.js +49 -0
  292. package/dist/src/utils/sanitize.d.ts +14 -0
  293. package/dist/src/utils/sanitize.js +186 -0
  294. package/dist/src/utils/semaphore.d.ts +22 -0
  295. package/dist/src/utils/semaphore.js +57 -0
  296. package/dist/src/utils/shutdown.d.ts +6 -0
  297. package/dist/src/utils/shutdown.js +51 -0
  298. package/dist/src/utils/tty.d.ts +5 -0
  299. package/dist/src/utils/tty.js +7 -0
  300. package/package.json +82 -0
@@ -0,0 +1,1008 @@
1
+ /**
2
+ * MagiRepl — Interactive REPL for the MAGI System.
3
+ *
4
+ * Provides a Claude Code-like experience: type questions naturally,
5
+ * context persists across turns, and the full-screen Evangelion TUI
6
+ * takes over during deliberation.
7
+ *
8
+ * Key design decision (Gemini review):
9
+ * readline is **close → recreate** per deliberation rather than
10
+ * pause/resume, to avoid stdin state conflicts with the TUI's raw mode.
11
+ */
12
+ import { createInterface } from 'node:readline/promises';
13
+ import * as readline from 'node:readline';
14
+ import { randomBytes } from 'node:crypto';
15
+ import chalk from 'chalk';
16
+ import { Magi } from '../index.js';
17
+ import { initializeTui } from '../cli/tui-setup.js';
18
+ import { CURSOR_SHOW, ALT_SCREEN_LEAVE, ERASE_LINE, ERASE_TO_END, moveUp } from '../tui/ansi.js';
19
+ import { EVA_PALETTE } from '../tui/colors.js';
20
+ import { logger } from '../utils/logger.js';
21
+ import { ReplContext } from './context.js';
22
+ import { createSlashCommands, parseInput, suggestCommand, } from './slash-commands.js';
23
+ import { JobRegistry } from './job-registry.js';
24
+ import { printBanner } from './banner.js';
25
+ import { assertTransition } from './repl-state.js';
26
+ import { buildPrompt, DEFAULT_SESSION_STATE } from './prompt-builder.js';
27
+ import { createReplHistory } from './history.js';
28
+ import { createCompleter } from './completer.js';
29
+ import { formatResultDisplay } from './result-display.js';
30
+ import { runReplBoot } from './boot-animation.js';
31
+ import { createReplEventListener } from './event-listener.js';
32
+ import { sanitizeForTerminal } from './terminal-sanitize.js';
33
+ import { estimateTokens } from '../metrics/token-tracker.js';
34
+ import { runHandoffAnimation } from './handoff-animation.js';
35
+ import { createMultilineCollector, isContinuationLine, joinContinuationLines, MAX_CONTINUATION_LINES } from './multiline-input.js';
36
+ import { createSessionStats, recordInput, recordSlashCommand, recordDeliberation, formatSessionSummary } from './session-stats.js';
37
+ import { shouldSkipAnimation } from './accessibility.js';
38
+ import { formatDeliberationAsJson, formatDeliberationAsMarkdown } from './export-formatter.js';
39
+ import { emitKeypressEvents } from 'node:readline';
40
+ import { lstat } from 'node:fs/promises';
41
+ import { resolve, relative } from 'node:path';
42
+ import { safeWriteFile } from '../utils/safe-fs.js';
43
+ import { createGhostTextEngine, isGhostTextDisabled } from './ghost-text.js';
44
+ import { loadUserConfigSafe, mergeWithDefaults } from '../config/user-config.js';
45
+ const { frame, abstain, approve, warning } = EVA_PALETTE;
46
+ const FRAME_CHALK = chalk.rgb(frame.r, frame.g, frame.b);
47
+ const DIM = chalk.rgb(abstain.r, abstain.g, abstain.b);
48
+ const OK_C = chalk.rgb(approve.r, approve.g, approve.b);
49
+ const WARN_C = chalk.rgb(warning.r, warning.g, warning.b);
50
+ const TRANSITION = FRAME_CHALK;
51
+ const MAX_INPUT_LENGTH = 100_000;
52
+ const ENGRAM_CONTEXT_CAP = 5_000;
53
+ const MAX_READLINE_RETRIES = 3;
54
+ // Module-scoped: emitKeypressEvents should only be called once per process.stdin
55
+ let keypressInitialized = false;
56
+ function isConfirmationDispatch(result) {
57
+ return typeof result === 'object' && result !== null && 'confirm' in result;
58
+ }
59
+ function isInteractiveDispatch(result) {
60
+ return typeof result === 'object' && result !== null && 'interactive' in result;
61
+ }
62
+ export class MagiRepl {
63
+ magi;
64
+ context;
65
+ slashCommands;
66
+ soundEnabled = false;
67
+ state = 'INIT';
68
+ abortController;
69
+ sessionState = { ...DEFAULT_SESSION_STATE };
70
+ history;
71
+ completerFn;
72
+ shownEngramIds = new Set();
73
+ stats = createSessionStats();
74
+ multilineCollector = createMultilineCollector();
75
+ lastSigintTime = 0;
76
+ lastDeliberation;
77
+ options;
78
+ ghostEngine;
79
+ jobRegistry = new JobRegistry();
80
+ currentRl;
81
+ watchJobId;
82
+ watchPollIntervalMs;
83
+ constructor(options) {
84
+ this.options = options ?? {};
85
+ this.magi = new Magi({ logLevel: 'warn', ...this.options.magiConfig });
86
+ this.context = new ReplContext();
87
+ this.slashCommands = createSlashCommands();
88
+ this.history = createReplHistory(this.options.historyDir);
89
+ this.soundEnabled = this.options.soundEnabled ?? false;
90
+ // Build completer from slash command registry
91
+ const completableCommands = [];
92
+ for (const [name, cmd] of this.slashCommands) {
93
+ completableCommands.push({ name, argKind: cmd.argKind, aliases: cmd.aliases });
94
+ }
95
+ // Ghost text engine — inline dim gray suggestions
96
+ // (initialized before completer so it can be passed as suppression source)
97
+ const ghostSource = {
98
+ getSlashCommands: () => [...this.slashCommands.keys()].map(k => `/${k}`),
99
+ getHistory: () => this.history.getEntries(),
100
+ getDeliberationTitles: () => this.context.getHistory().map(e => e.title),
101
+ };
102
+ this.ghostEngine = createGhostTextEngine(ghostSource, {
103
+ enabled: !isGhostTextDisabled(),
104
+ });
105
+ this.completerFn = createCompleter(completableCommands, {
106
+ ghostEngine: this.ghostEngine,
107
+ });
108
+ }
109
+ get currentState() {
110
+ return this.state;
111
+ }
112
+ get running() {
113
+ return this.state !== 'DISPOSING';
114
+ }
115
+ transition(to) {
116
+ const from = this.state;
117
+ assertTransition(from, to);
118
+ this.state = to;
119
+ logger.debug('REPL state transition', { from, to });
120
+ }
121
+ async start() {
122
+ this.transition('READY');
123
+ // First-run: auto-launch setup if no config exists
124
+ const existingConfig = await loadUserConfigSafe();
125
+ if (!existingConfig) {
126
+ const { runSetup } = await import('../cli/commands/setup.js');
127
+ await runSetup();
128
+ // Rebuild Magi instance with the newly saved config
129
+ const freshConfig = await loadUserConfigSafe();
130
+ if (freshConfig) {
131
+ const merged = mergeWithDefaults(freshConfig);
132
+ this.magi = new Magi({ logLevel: 'warn', ...merged });
133
+ this.soundEnabled = freshConfig.tui?.soundEnabled ?? false;
134
+ }
135
+ }
136
+ // Boot animation (replaces static banner)
137
+ if (!this.options.skipBootAnimation) {
138
+ await runReplBoot({ write: s => process.stdout.write(s) });
139
+ }
140
+ // Load history for readline
141
+ const historyEntries = await this.history.load();
142
+ // Startup health check (non-blocking, updates prompt)
143
+ this.checkUnitsOnStartup();
144
+ while (this.running) {
145
+ const pendingTask = await this.readInputLoop(historyEntries);
146
+ if (!pendingTask)
147
+ break; // quit or EOF
148
+ if (pendingTask === 'continue')
149
+ continue;
150
+ await this.runDeliberation(pendingTask.task, pendingTask.berserk);
151
+ }
152
+ await this.dispose();
153
+ }
154
+ // ── Input loop ──────────────────────────────────────────────
155
+ async readInputLoop(historyEntries, retryCount = 0) {
156
+ this.transition('READING_INPUT');
157
+ let rl;
158
+ try {
159
+ rl = this.createReadline(historyEntries);
160
+ }
161
+ catch (error) {
162
+ logger.debug('readInputLoop: createReadline failed', { error: String(error) });
163
+ console.error(chalk.red(`\n Failed to create readline: ${String(error)}\n`));
164
+ this.sessionState.hasError = true;
165
+ if (this.running && retryCount < MAX_READLINE_RETRIES) {
166
+ return this.readInputLoop(historyEntries, retryCount + 1);
167
+ }
168
+ return undefined;
169
+ }
170
+ // Continuation line buffer (for trailing-backslash mode)
171
+ let continuationBuffer = [];
172
+ try {
173
+ for await (const line of rl) {
174
+ const trimmed = line.trim();
175
+ // ── Multiline collecting mode (/paste) ──
176
+ if (this.state === 'MULTILINE_COLLECTING') {
177
+ const res = this.multilineCollector.feed(line);
178
+ if (res.status === 'collecting') {
179
+ rl.setPrompt(DIM('... '));
180
+ rl.prompt();
181
+ continue;
182
+ }
183
+ this.multilineCollector.reset();
184
+ this.transition('READING_INPUT');
185
+ if (res.status === 'aborted') {
186
+ console.log(DIM(' Multiline input aborted.'));
187
+ rl.setPrompt(buildPrompt(this.sessionState));
188
+ rl.prompt();
189
+ continue;
190
+ }
191
+ // res.status === 'complete'
192
+ if (!res.text.trim()) {
193
+ rl.setPrompt(buildPrompt(this.sessionState));
194
+ rl.prompt();
195
+ continue;
196
+ }
197
+ // Treat collected text as deliberation input
198
+ const result = await this.handleInput(res.text.trim());
199
+ if (!result || result === 'paste') {
200
+ this.transition('READING_INPUT');
201
+ rl.setPrompt(buildPrompt(this.sessionState));
202
+ rl.prompt();
203
+ continue;
204
+ }
205
+ if (result === 'quit') {
206
+ rl.close();
207
+ return undefined;
208
+ }
209
+ if (isConfirmationDispatch(result)) {
210
+ rl.close();
211
+ await this.handleConfirmation(result.confirm);
212
+ return 'continue';
213
+ }
214
+ if (isInteractiveDispatch(result)) {
215
+ rl.close();
216
+ try {
217
+ await result.interactive();
218
+ }
219
+ catch { /* handled inline */ }
220
+ return 'continue';
221
+ }
222
+ rl.close();
223
+ this.transition('READY');
224
+ return result;
225
+ }
226
+ // ── Continuation line mode (trailing backslash) ──
227
+ if (continuationBuffer.length > 0) {
228
+ continuationBuffer.push(line);
229
+ if (continuationBuffer.length > MAX_CONTINUATION_LINES) {
230
+ console.log(chalk.yellow(`\n Continuation too long (max ${MAX_CONTINUATION_LINES} lines). Input discarded.\n`));
231
+ continuationBuffer = [];
232
+ rl.setPrompt(buildPrompt(this.sessionState));
233
+ rl.prompt();
234
+ continue;
235
+ }
236
+ if (!isContinuationLine(trimmed)) {
237
+ // Last line — join and process
238
+ const joined = joinContinuationLines(continuationBuffer);
239
+ continuationBuffer = [];
240
+ this.transition('READING_INPUT');
241
+ const result = await this.handleInput(joined.trim());
242
+ if (!result || result === 'paste') {
243
+ this.transition('READING_INPUT');
244
+ rl.setPrompt(buildPrompt(this.sessionState));
245
+ rl.prompt();
246
+ continue;
247
+ }
248
+ if (result === 'quit') {
249
+ rl.close();
250
+ return undefined;
251
+ }
252
+ if (isConfirmationDispatch(result)) {
253
+ rl.close();
254
+ await this.handleConfirmation(result.confirm);
255
+ return 'continue';
256
+ }
257
+ if (isInteractiveDispatch(result)) {
258
+ rl.close();
259
+ try {
260
+ await result.interactive();
261
+ }
262
+ catch { /* handled inline */ }
263
+ return 'continue';
264
+ }
265
+ rl.close();
266
+ this.transition('READY');
267
+ return result;
268
+ }
269
+ // Still continuing
270
+ rl.setPrompt(DIM('... '));
271
+ rl.prompt();
272
+ continue;
273
+ }
274
+ if (!trimmed)
275
+ continue;
276
+ if (trimmed.length > MAX_INPUT_LENGTH) {
277
+ console.log(chalk.yellow(`\n Input too long (${trimmed.length.toLocaleString()} chars, max ${MAX_INPUT_LENGTH.toLocaleString()}). Please shorten.\n`));
278
+ continue;
279
+ }
280
+ // Check for continuation line
281
+ if (isContinuationLine(trimmed)) {
282
+ continuationBuffer = [line];
283
+ rl.setPrompt(DIM('... '));
284
+ rl.prompt();
285
+ continue;
286
+ }
287
+ // Record to persistent history
288
+ this.history.addEntry(trimmed);
289
+ // Reset error state on valid input
290
+ this.sessionState.hasError = false;
291
+ const result = await this.handleInput(trimmed);
292
+ if (!result) {
293
+ // Handled inline, keep reading
294
+ this.transition('READING_INPUT');
295
+ rl.setPrompt(buildPrompt(this.sessionState));
296
+ rl.prompt();
297
+ continue;
298
+ }
299
+ if (result === 'quit') {
300
+ rl.close();
301
+ return undefined;
302
+ }
303
+ if (result === 'paste') {
304
+ // Enter multiline collecting mode
305
+ this.multilineCollector.reset();
306
+ this.transition('MULTILINE_COLLECTING');
307
+ console.log(DIM(' Paste your text. End with "." on its own line. /abort to cancel.'));
308
+ rl.setPrompt(DIM('... '));
309
+ rl.prompt();
310
+ continue;
311
+ }
312
+ if (isConfirmationDispatch(result)) {
313
+ rl.close();
314
+ await this.handleConfirmation(result.confirm);
315
+ return 'continue';
316
+ }
317
+ if (isInteractiveDispatch(result)) {
318
+ // Close readline to release stdin for the interactive UI
319
+ rl.close();
320
+ try {
321
+ await result.interactive();
322
+ }
323
+ catch (error) {
324
+ console.error(WARN_C(` コマンド実行異常: ${String(error)}`));
325
+ this.sessionState.hasError = true;
326
+ }
327
+ return 'continue'; // readInputLoop will be re-entered with fresh readline
328
+ }
329
+ // Deliberation requested — close readline and return task
330
+ rl.close();
331
+ this.transition('READY');
332
+ return result;
333
+ }
334
+ }
335
+ catch (error) {
336
+ const isNormalClose = error instanceof Error &&
337
+ 'code' in error && error.code === 'ERR_USE_AFTER_CLOSE';
338
+ if (!isNormalClose) {
339
+ logger.debug('readInputLoop: readline error', { error: String(error) });
340
+ console.error(chalk.red(`\n Readline error: ${String(error)}\n`));
341
+ this.sessionState.hasError = true;
342
+ // Recover: recreate readline with retry limit
343
+ if (this.running && retryCount < MAX_READLINE_RETRIES) {
344
+ try {
345
+ rl.close();
346
+ }
347
+ catch { /* best-effort */ }
348
+ return this.readInputLoop(historyEntries, retryCount + 1);
349
+ }
350
+ }
351
+ }
352
+ // EOF (Ctrl+D)
353
+ return undefined;
354
+ }
355
+ createReadline(historyEntries) {
356
+ const prompt = buildPrompt(this.sessionState);
357
+ // P0-3: autoClose: false prevents stdin from being destroyed on rl.close(),
358
+ // allowing readline to be recreated per deliberation cycle.
359
+ const rl = createInterface({
360
+ input: process.stdin,
361
+ output: process.stdout,
362
+ terminal: process.stdin.isTTY ?? false,
363
+ prompt,
364
+ completer: this.completerFn,
365
+ history: historyEntries,
366
+ historySize: 1000,
367
+ removeHistoryDuplicates: true,
368
+ autoClose: false,
369
+ });
370
+ rl.on('SIGINT', () => {
371
+ const now = Date.now();
372
+ if (now - this.lastSigintTime < 1500) {
373
+ // Double-tap → graceful exit
374
+ console.log('');
375
+ rl.close();
376
+ return;
377
+ }
378
+ this.lastSigintTime = now;
379
+ rl.write('', { ctrl: true, name: 'u' });
380
+ console.log('');
381
+ console.log(DIM(' (Press Ctrl+C again to exit)'));
382
+ rl.setPrompt(buildPrompt(this.sessionState));
383
+ rl.prompt();
384
+ });
385
+ // Ghost text: keypress-driven inline suggestions (TTY only)
386
+ if (process.stdin.isTTY && !isGhostTextDisabled()) {
387
+ if (!keypressInitialized) {
388
+ emitKeypressEvents(process.stdin);
389
+ keypressInitialized = true;
390
+ }
391
+ let rlClosed = false;
392
+ const ghostHandler = (_str, key) => {
393
+ if (!key || rlClosed)
394
+ return;
395
+ // Tab → accept ghost suggestion
396
+ if (key.name === 'tab' && !key.shift) {
397
+ const accepted = this.ghostEngine.accept();
398
+ if (accepted) {
399
+ rl.write(accepted);
400
+ return;
401
+ }
402
+ }
403
+ // After any other key, update ghost text on next tick
404
+ // Uses Node.js internal `rl.line` (stable since v0.x, not in public API)
405
+ setImmediate(() => {
406
+ if (rlClosed)
407
+ return;
408
+ const line = rl.line ?? '';
409
+ const cursor = rl.cursor ?? line.length;
410
+ const promptText = buildPrompt(this.sessionState);
411
+ this.ghostEngine.clear(s => process.stdout.write(s));
412
+ if (cursor !== line.length) {
413
+ return;
414
+ }
415
+ const suggestion = this.ghostEngine.suggest(line);
416
+ this.ghostEngine.render(line, promptText, suggestion, s => process.stdout.write(s));
417
+ });
418
+ };
419
+ process.stdin.on('keypress', ghostHandler);
420
+ // Cleanup on readline close — prevents post-close ghost writes
421
+ rl.once('close', () => {
422
+ rlClosed = true;
423
+ process.stdin.removeListener('keypress', ghostHandler);
424
+ this.ghostEngine.clear(s => process.stdout.write(s));
425
+ });
426
+ }
427
+ this.currentRl = rl;
428
+ rl.once('close', () => { this.currentRl = undefined; });
429
+ rl.prompt();
430
+ return rl;
431
+ }
432
+ // ── Input dispatch ────────────────────────────────────────
433
+ async handleInput(input) {
434
+ this.transition('DISPATCHING');
435
+ recordInput(this.stats);
436
+ const parsed = parseInput(input);
437
+ if (parsed.isSlash) {
438
+ return this.handleSlashCommand(parsed.command, parsed.args);
439
+ }
440
+ // Natural language → deliberation
441
+ const task = {
442
+ type: 'custom',
443
+ title: input.slice(0, 80),
444
+ description: input,
445
+ context: this.context.buildContextString(),
446
+ };
447
+ // EngRam: inject related past deliberations
448
+ this.injectEngramContext(task);
449
+ return { task, berserk: false };
450
+ }
451
+ async handleSlashCommand(command, args) {
452
+ const cmd = this.slashCommands.get(command);
453
+ if (!cmd) {
454
+ const suggestion = suggestCommand(command, this.slashCommands.keys());
455
+ const hint = suggestion ? ` Did you mean /${suggestion}?` : '';
456
+ console.log(WARN_C(` 未定義のコマンドパターン: /${command}.${hint} Type /help for available commands.`));
457
+ this.sessionState.hasError = true;
458
+ return undefined;
459
+ }
460
+ recordSlashCommand(this.stats);
461
+ let result;
462
+ try {
463
+ result = await cmd.execute(args, this.magi, this.context);
464
+ }
465
+ catch (error) {
466
+ console.error(WARN_C(` コマンド実行異常: ${String(error)}`));
467
+ this.sessionState.hasError = true;
468
+ return undefined;
469
+ }
470
+ if (result.output) {
471
+ console.log(result.output);
472
+ }
473
+ if (result.action === 'quit') {
474
+ return 'quit';
475
+ }
476
+ if (result.action === 'paste') {
477
+ return 'paste';
478
+ }
479
+ if (result.action === 'clear') {
480
+ console.clear();
481
+ printBanner();
482
+ return undefined;
483
+ }
484
+ if (result.action === 'reset') {
485
+ this.context.clear();
486
+ this.sessionState.contextEntryCount = 0;
487
+ console.log(DIM(' Context reset. Deliberation history cleared.'));
488
+ return undefined;
489
+ }
490
+ if (result.action === 'toggle-sound') {
491
+ this.soundEnabled = !this.soundEnabled;
492
+ const status = this.soundEnabled ? OK_C('ON ♪') : DIM('OFF');
493
+ console.log(` ${FRAME_CHALK('SOUND:')} ${status}`);
494
+ return undefined;
495
+ }
496
+ if (result.action === 'export') {
497
+ await this.exportDeliberation(result.output || '');
498
+ return undefined;
499
+ }
500
+ if (result.action === 'jobs') {
501
+ this.handleJobsCommand(result.output || '');
502
+ return undefined;
503
+ }
504
+ if (result.watch) {
505
+ this.handleWatchCommand(result.watch);
506
+ return undefined;
507
+ }
508
+ // Handle interactive commands (need exclusive stdin access)
509
+ // Return to readInputLoop which will close readline before executing.
510
+ if (result.interactive) {
511
+ return { interactive: result.interactive };
512
+ }
513
+ // Handle background job request
514
+ if (result.background) {
515
+ const bg = result.background;
516
+ try {
517
+ this.jobRegistry.add(bg.name, async (signal) => {
518
+ await bg.run(signal);
519
+ }, {
520
+ onSettled: (job) => {
521
+ this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
522
+ if (job.status === 'failed' && job.error) {
523
+ this.writeAbovePrompt(WARN_C(` Job "${bg.name}" failed: ${job.error}`));
524
+ }
525
+ else if (!job.abort.signal.aborted) {
526
+ this.writeAbovePrompt(DIM(` Job "${bg.name}" completed.`));
527
+ }
528
+ },
529
+ });
530
+ this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
531
+ }
532
+ catch (error) {
533
+ this.sessionState.hasError = true;
534
+ console.error(WARN_C(` コマンド実行異常: ${String(error)}`));
535
+ }
536
+ return undefined;
537
+ }
538
+ // Handle confirmation request (dangerous operations)
539
+ if (result.confirm) {
540
+ return { confirm: result.confirm };
541
+ }
542
+ if (result.deliberation) {
543
+ // Merge auto-collected context with REPL session context
544
+ const sessionContext = this.context.buildContextString();
545
+ const autoContext = result.deliberation.task.context;
546
+ let mergedContext;
547
+ if (sessionContext && autoContext) {
548
+ mergedContext = `${autoContext}\n\n${sessionContext}`;
549
+ }
550
+ else {
551
+ mergedContext = autoContext ?? sessionContext;
552
+ }
553
+ const task = {
554
+ ...result.deliberation.task,
555
+ context: mergedContext,
556
+ };
557
+ // EngRam: inject related past deliberations
558
+ this.injectEngramContext(task);
559
+ return {
560
+ task,
561
+ berserk: result.deliberation.berserk ?? false,
562
+ };
563
+ }
564
+ return undefined;
565
+ }
566
+ // ── Confirmation flow (dangerous operations) ────────────────
567
+ async handleConfirmation(confirm) {
568
+ this.transition('CONFIRMING');
569
+ console.log(confirm.description);
570
+ const required = confirm.requiredInput ?? this.createConfirmationToken();
571
+ const timeoutMs = confirm.timeoutMs ?? 30_000;
572
+ try {
573
+ await this.drainPendingInput();
574
+ const rl = createInterface({
575
+ input: process.stdin,
576
+ output: process.stdout,
577
+ terminal: process.stdin.isTTY ?? false,
578
+ autoClose: false,
579
+ });
580
+ const ac = new AbortController();
581
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
582
+ let answer;
583
+ try {
584
+ answer = await rl.question(DIM(` 続行するには "${required}" と入力してください (${Math.round(timeoutMs / 1000)}秒でタイムアウト): `), { signal: ac.signal });
585
+ }
586
+ catch (error) {
587
+ if (ac.signal.aborted || (error instanceof Error && error.name === 'AbortError')) {
588
+ console.log(WARN_C('\n タイムアウト — 操作を中止しました。'));
589
+ }
590
+ else if (error instanceof Error && (error.message.includes('readline was closed') || error.code === 'ERR_USE_AFTER_CLOSE')) {
591
+ console.log(WARN_C('\n 操作を中止しました。'));
592
+ }
593
+ else {
594
+ this.sessionState.hasError = true;
595
+ console.error(WARN_C(` 確認入力異常: ${String(error)}`));
596
+ }
597
+ return;
598
+ }
599
+ finally {
600
+ clearTimeout(timer);
601
+ rl.close();
602
+ }
603
+ if (answer.trim() !== required) {
604
+ console.log(DIM(' 操作を中止しました。'));
605
+ return;
606
+ }
607
+ this.transition('DISPATCHING');
608
+ try {
609
+ const result = await confirm.execute();
610
+ if (result.output) {
611
+ console.log(result.output);
612
+ }
613
+ }
614
+ catch (error) {
615
+ this.sessionState.hasError = true;
616
+ console.error(WARN_C(` 操作実行異常: ${String(error)}`));
617
+ }
618
+ }
619
+ catch (error) {
620
+ this.sessionState.hasError = true;
621
+ logger.debug('Confirmation flow error', { error: String(error) });
622
+ console.error(WARN_C(` 確認フロー異常: ${String(error)}`));
623
+ }
624
+ finally {
625
+ // Return to READY so the next loop can recreate readline cleanly.
626
+ if (this.state === 'CONFIRMING' || this.state === 'DISPATCHING') {
627
+ this.transition('READY');
628
+ }
629
+ }
630
+ }
631
+ // ── Background job management ──────────────────────────────
632
+ async drainPendingInput() {
633
+ await new Promise(resolve => setImmediate(resolve));
634
+ if (!process.stdin.readable)
635
+ return;
636
+ while (process.stdin.read() !== null) {
637
+ // Discard buffered input so confirmation requires a new explicit entry.
638
+ }
639
+ }
640
+ createConfirmationToken() {
641
+ return `yes-${randomBytes(2).toString('hex')}`;
642
+ }
643
+ handleWatchCommand(command) {
644
+ if (command.command === 'status') {
645
+ const activeJob = this.watchJobId === undefined ? undefined : this.jobRegistry.get(this.watchJobId);
646
+ if (!activeJob || activeJob.status !== 'running') {
647
+ console.log(DIM(' Angel watch is stopped.'));
648
+ return;
649
+ }
650
+ const intervalLabel = this.watchPollIntervalMs ? ` (${this.watchPollIntervalMs}ms)` : '';
651
+ console.log(OK_C(` Angel watch is running as job #${activeJob.id}${intervalLabel}.`));
652
+ return;
653
+ }
654
+ if (command.command === 'stop') {
655
+ const activeJob = this.watchJobId === undefined ? undefined : this.jobRegistry.get(this.watchJobId);
656
+ if (!activeJob || activeJob.status !== 'running') {
657
+ console.log(DIM(' Angel watch is not running.'));
658
+ return;
659
+ }
660
+ this.magi.runtime.stopWatch();
661
+ this.jobRegistry.cancel(activeJob.id);
662
+ this.watchJobId = undefined;
663
+ this.watchPollIntervalMs = undefined;
664
+ this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
665
+ console.log(DIM(' Angel watch stopped.'));
666
+ return;
667
+ }
668
+ const activeJob = this.watchJobId === undefined ? undefined : this.jobRegistry.get(this.watchJobId);
669
+ if (activeJob && activeJob.status === 'running') {
670
+ console.log(WARN_C(` Angel watch is already running as job #${activeJob.id}.`));
671
+ return;
672
+ }
673
+ try {
674
+ const job = this.jobRegistry.add('Angel Watch', async (signal) => {
675
+ this.magi.runtime.startWatch({ pollIntervalMs: command.pollIntervalMs, signal });
676
+ await new Promise((resolve) => {
677
+ signal.addEventListener('abort', () => resolve(), { once: true });
678
+ });
679
+ }, {
680
+ onSettled: (settledJob) => {
681
+ if (this.watchJobId === settledJob.id) {
682
+ this.watchJobId = undefined;
683
+ this.watchPollIntervalMs = undefined;
684
+ }
685
+ this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
686
+ if (settledJob.status === 'failed' && settledJob.error) {
687
+ this.writeAbovePrompt(WARN_C(` Job "${settledJob.name}" failed: ${settledJob.error}`));
688
+ return;
689
+ }
690
+ if (!settledJob.abort.signal.aborted) {
691
+ this.writeAbovePrompt(DIM(` Job "${settledJob.name}" completed.`));
692
+ }
693
+ },
694
+ });
695
+ this.watchJobId = job.id;
696
+ this.watchPollIntervalMs = command.pollIntervalMs;
697
+ this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
698
+ console.log(OK_C(` Angel watch started as job #${job.id}${command.pollIntervalMs ? ` (${command.pollIntervalMs}ms)` : ''}.`));
699
+ }
700
+ catch (error) {
701
+ this.sessionState.hasError = true;
702
+ console.error(WARN_C(` Angel watch failed: ${String(error)}`));
703
+ }
704
+ }
705
+ handleJobsCommand(args) {
706
+ const parts = args.trim().split(/\s+/);
707
+ if (parts[0] === 'cancel' && parts[1]) {
708
+ const id = parseInt(parts[1], 10);
709
+ if (Number.isNaN(id)) {
710
+ console.log(WARN_C(' Usage: /jobs cancel <id>'));
711
+ return;
712
+ }
713
+ const ok = this.jobRegistry.cancel(id);
714
+ if (ok) {
715
+ if (this.watchJobId === id) {
716
+ this.magi.runtime.stopWatch();
717
+ this.watchJobId = undefined;
718
+ this.watchPollIntervalMs = undefined;
719
+ }
720
+ this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
721
+ console.log(OK_C(` Job #${id} cancelled.`));
722
+ }
723
+ else {
724
+ console.log(WARN_C(` Job #${id} not found or already finished.`));
725
+ }
726
+ return;
727
+ }
728
+ const jobs = this.jobRegistry.list();
729
+ if (jobs.length === 0) {
730
+ console.log(DIM(' No background jobs.'));
731
+ return;
732
+ }
733
+ const lines = ['', FRAME_CHALK.bold(' Background Jobs:'), ''];
734
+ for (const job of jobs) {
735
+ const elapsed = Math.round((Date.now() - job.startedAt) / 1000);
736
+ const statusColor = job.status === 'running' ? OK_C
737
+ : job.status === 'failed' ? WARN_C : DIM;
738
+ lines.push(` #${job.id} ${statusColor(job.status.toUpperCase().padEnd(7))} ${job.name} (${elapsed}s)`);
739
+ if (job.error)
740
+ lines.push(` ${WARN_C(job.error)}`);
741
+ }
742
+ lines.push('');
743
+ console.log(lines.join('\n'));
744
+ }
745
+ writeAbovePrompt(message) {
746
+ if (!this.currentRl)
747
+ return;
748
+ const output = process.stdout;
749
+ readline.clearLine(output, 0);
750
+ readline.cursorTo(output, 0);
751
+ output.write(message + '\n');
752
+ this.currentRl.prompt(true);
753
+ }
754
+ // ── EngRam integration ──────────────────────────────────────
755
+ injectEngramContext(task) {
756
+ try {
757
+ const engram = this.magi.getEngramManager();
758
+ const query = `${task.title} ${task.description}`;
759
+ const similar = engram.findSimilar(query, 3);
760
+ // Deduplicate: filter out entries already in ReplContext or shown this session
761
+ const contextIds = new Set(this.context.getHistory().map(e => e.id));
762
+ const filtered = similar.filter(m => !contextIds.has(m.deliberationId) && !this.shownEngramIds.has(m.deliberationId));
763
+ if (filtered.length === 0)
764
+ return;
765
+ // Track shown IDs at session level to avoid re-injection
766
+ for (const m of filtered) {
767
+ this.shownEngramIds.add(m.deliberationId);
768
+ }
769
+ // Show related deliberations (sanitize external data)
770
+ console.log(DIM(' Related deliberations:'));
771
+ for (const m of filtered) {
772
+ console.log(DIM(` ⬢ ${sanitizeForTerminal(m.taskTitle)} (${m.decision})`));
773
+ }
774
+ // Inject into context (capped)
775
+ const engramContext = filtered
776
+ .map(m => `[Past] "${m.taskTitle}" → ${m.decision}: ${(m.keyPoints || []).slice(0, 2).join(', ')}`)
777
+ .join('\n')
778
+ .slice(0, ENGRAM_CONTEXT_CAP);
779
+ task.context = [task.context, engramContext].filter(Boolean).join('\n\n');
780
+ }
781
+ catch {
782
+ // EngRam is optional — graceful degradation
783
+ logger.debug('EngRam context injection failed');
784
+ }
785
+ }
786
+ // ── Deliberation execution ────────────────────────────────
787
+ async runDeliberation(task, berserk) {
788
+ this.transition('DELIBERATING');
789
+ // Update session state
790
+ this.sessionState.berserkActive = berserk;
791
+ let handoffLineCount;
792
+ // Handoff animation before TUI takes over
793
+ if (shouldSkipAnimation()) {
794
+ console.log('');
795
+ console.log(TRANSITION(' DELIBERATION SEQUENCE — 思考回路 展開'));
796
+ console.log('');
797
+ handoffLineCount = 3;
798
+ }
799
+ else {
800
+ handoffLineCount = await runHandoffAnimation({
801
+ write: s => process.stdout.write(s),
802
+ taskTitle: task.title,
803
+ berserk,
804
+ unitCount: this.sessionState.totalUnits,
805
+ });
806
+ }
807
+ this.abortController = new AbortController();
808
+ const { signal } = this.abortController;
809
+ // Ctrl+C during deliberation → abort instead of default behavior
810
+ const cancelHandler = () => { this.abortController?.abort(); };
811
+ process.once('SIGINT', cancelHandler);
812
+ let tuiInstance;
813
+ let eventListener;
814
+ let disposeStreaming;
815
+ try {
816
+ const setup = await initializeTui(this.magi.getEventBus(), this.options.tuiEnabled ?? true, { soundEnabled: this.soundEnabled });
817
+ tuiInstance = setup.tuiInstance;
818
+ disposeStreaming = setup.disposeStreaming;
819
+ // If TUI is not available AND no streaming reporter active, use inline listener
820
+ if (!tuiInstance && !setup.streamingActive) {
821
+ eventListener = createReplEventListener({
822
+ eventBus: this.magi.getEventBus(),
823
+ write: s => process.stdout.write(s),
824
+ });
825
+ eventListener.subscribe();
826
+ }
827
+ let result;
828
+ if (berserk) {
829
+ result = await this.magi.deliberateBerserk(task, { signal });
830
+ }
831
+ else {
832
+ result = await this.magi.deliberate(task, { signal });
833
+ }
834
+ if (tuiInstance)
835
+ await tuiInstance.waitForDismiss();
836
+ tuiInstance?.dispose();
837
+ tuiInstance = undefined;
838
+ this.transition('SHOWING_RESULT');
839
+ if (handoffLineCount > 0) {
840
+ process.stdout.write(this.clearRecentLines(handoffLineCount));
841
+ }
842
+ process.stdout.write(ERASE_TO_END);
843
+ // Rich result display (replaces old printResultSummary)
844
+ console.log(formatResultDisplay(result));
845
+ this.lastDeliberation = result;
846
+ this.context.add(result);
847
+ this.sessionState.deliberationCount++;
848
+ this.sessionState.contextEntryCount = this.context.getHistory().length;
849
+ this.sessionState.berserkActive = false;
850
+ // Record session statistics — estimate tokens using CJK-aware heuristic
851
+ const tokens = result.rounds.reduce((sum, r) => sum + (r.opinions?.reduce((s, o) => s + estimateTokens(o.rawOutput ?? ''), 0) ?? 0), 0);
852
+ recordDeliberation(this.stats, result.consensus.decision, result.totalDurationMs, tokens, berserk);
853
+ this.stats.contextEntriesUsed = this.context.getHistory().length;
854
+ this.transition('READY');
855
+ }
856
+ catch (error) {
857
+ // Safety net: ensure terminal is restored
858
+ if (tuiInstance) {
859
+ try {
860
+ tuiInstance.dispose();
861
+ }
862
+ catch { /* intentional: cleanup must not throw */ }
863
+ }
864
+ // P0-4: Force-restore raw mode in case TUI dispose() failed
865
+ if (process.stdin.isTTY && process.stdin.isRaw) {
866
+ try {
867
+ process.stdin.setRawMode(false);
868
+ }
869
+ catch { /* best-effort */ }
870
+ }
871
+ process.stdout.write(CURSOR_SHOW + ALT_SCREEN_LEAVE);
872
+ const msg = signal.aborted ? '思考ルーチン中断' : `思考ルーチンに異常発生: ${String(error)}`;
873
+ console.error(WARN_C(`\n ${msg}\n`));
874
+ this.sessionState.hasError = true;
875
+ this.sessionState.berserkActive = false;
876
+ // Recover to READY if not already disposing
877
+ if (this.state !== 'DISPOSING') {
878
+ const from = this.state;
879
+ this.state = 'READY'; // Direct assignment: error recovery from DELIBERATING or SHOWING_RESULT
880
+ logger.debug('REPL state transition (recovery)', { from, to: 'READY' });
881
+ }
882
+ }
883
+ finally {
884
+ eventListener?.unsubscribe();
885
+ disposeStreaming?.();
886
+ process.removeListener('SIGINT', cancelHandler);
887
+ this.abortController = undefined;
888
+ }
889
+ }
890
+ // ── Lifecycle ─────────────────────────────────────────────
891
+ checkUnitsOnStartup() {
892
+ // Fire-and-forget: update prompt with actual unit status
893
+ this.magi.healthCheck().then(results => {
894
+ let online = 0;
895
+ let total = 0;
896
+ for (const [, result] of results) {
897
+ total++;
898
+ if (!(result instanceof Error))
899
+ online++;
900
+ }
901
+ this.sessionState.onlineUnits = online;
902
+ this.sessionState.totalUnits = total;
903
+ }).catch(() => {
904
+ // Keep default 3/3 on failure
905
+ });
906
+ }
907
+ /**
908
+ * Graceful resource cleanup — idempotent, callable from any state.
909
+ * Use for programmatic shutdown without process.exit().
910
+ */
911
+ async dispose() {
912
+ if (this.state === 'DISPOSING')
913
+ return;
914
+ // Abort in-flight deliberation
915
+ if (this.abortController) {
916
+ this.abortController.abort();
917
+ this.abortController = undefined;
918
+ }
919
+ // Direct state assignment: dispose must succeed from ANY state
920
+ const from = this.state;
921
+ this.state = 'DISPOSING';
922
+ logger.debug('REPL state transition (dispose)', { from, to: 'DISPOSING' });
923
+ // Abort background jobs and flush history
924
+ this.jobRegistry.dispose();
925
+ await this.history.flush();
926
+ this.history.dispose();
927
+ this.ghostEngine.dispose();
928
+ // Session summary (only if deliberations were run)
929
+ const summary = formatSessionSummary(this.stats);
930
+ if (summary) {
931
+ console.log('');
932
+ console.log(summary);
933
+ }
934
+ // Terminal state restoration
935
+ if (process.stdin.isTTY && process.stdin.isRaw) {
936
+ try {
937
+ process.stdin.setRawMode(false);
938
+ }
939
+ catch { /* best-effort */ }
940
+ }
941
+ process.stdout.write(CURSOR_SHOW + ALT_SCREEN_LEAVE);
942
+ console.log('');
943
+ console.log(DIM(' MAGI SYSTEM — session terminated — 全回路 正常切断'));
944
+ console.log('');
945
+ }
946
+ // ── Export ────────────────────────────────────────────────
947
+ async exportDeliberation(args) {
948
+ const flagJson = args.includes('--json');
949
+ const cleanArgs = args.replace('--json', '').trim();
950
+ // Generate filename — infer format from extension if user specified a filename
951
+ const now = new Date();
952
+ const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
953
+ const isJson = cleanArgs
954
+ ? flagJson || cleanArgs.endsWith('.json')
955
+ : flagJson;
956
+ const ext = isJson ? 'json' : 'md';
957
+ const filename = cleanArgs || `magi-export-${ts}.${ext}`;
958
+ // Security: path traversal defense BEFORE deliberation check
959
+ if (filename.includes('\0')) {
960
+ console.log(WARN_C(' Error: invalid file path.'));
961
+ return;
962
+ }
963
+ const cwd = process.cwd();
964
+ const filePath = resolve(cwd, filename);
965
+ const rel = relative(cwd, filePath);
966
+ if (rel.startsWith('..') || resolve(rel) === rel) {
967
+ console.log(WARN_C(' Error: path traversal denied.'));
968
+ return;
969
+ }
970
+ if (!this.lastDeliberation) {
971
+ console.log(WARN_C(' No deliberation to export.'));
972
+ return;
973
+ }
974
+ const content = isJson
975
+ ? formatDeliberationAsJson(this.lastDeliberation)
976
+ : formatDeliberationAsMarkdown(this.lastDeliberation);
977
+ try {
978
+ // Security: reject symlinks and warn on existing files
979
+ try {
980
+ const stat = await lstat(filePath);
981
+ if (stat.isSymbolicLink()) {
982
+ console.log(WARN_C(' Error: symlink target rejected.'));
983
+ return;
984
+ }
985
+ // File exists — overwrite with warning
986
+ console.log(DIM(` (overwriting existing ${rel})`));
987
+ }
988
+ catch {
989
+ // File does not exist — OK to create
990
+ }
991
+ await safeWriteFile(filePath, content);
992
+ console.log(OK_C(` Exported to ${rel}`));
993
+ }
994
+ catch (error) {
995
+ console.error(WARN_C(` Export failed: ${String(error)}`));
996
+ }
997
+ }
998
+ clearRecentLines(lineCount) {
999
+ let sequence = '';
1000
+ for (let i = 0; i < lineCount; i++) {
1001
+ sequence += `\r${ERASE_LINE}`;
1002
+ if (i < lineCount - 1) {
1003
+ sequence += moveUp(1);
1004
+ }
1005
+ }
1006
+ return sequence;
1007
+ }
1008
+ }