aimux-cli 0.1.15 → 0.1.18

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 (299) hide show
  1. package/README.md +184 -67
  2. package/bin/aimux-dev +10 -0
  3. package/dist/alert-display.d.ts +21 -0
  4. package/dist/alert-display.js +86 -0
  5. package/dist/alert-display.js.map +1 -0
  6. package/dist/attachment-store.d.ts +0 -7
  7. package/dist/attachment-store.js +2 -86
  8. package/dist/attachment-store.js.map +1 -1
  9. package/dist/builtin-metadata-watchers.js +4 -4
  10. package/dist/builtin-metadata-watchers.js.map +1 -1
  11. package/dist/claude-hooks.d.ts +1 -0
  12. package/dist/claude-hooks.js +25 -0
  13. package/dist/claude-hooks.js.map +1 -1
  14. package/dist/config.d.ts +19 -13
  15. package/dist/config.js +28 -14
  16. package/dist/config.js.map +1 -1
  17. package/dist/connection-targets.d.ts +8 -0
  18. package/dist/connection-targets.js +28 -0
  19. package/dist/connection-targets.js.map +1 -0
  20. package/dist/credentials.d.ts +12 -0
  21. package/dist/credentials.js +49 -0
  22. package/dist/credentials.js.map +1 -0
  23. package/dist/daemon.d.ts +23 -0
  24. package/dist/daemon.js +391 -66
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/dashboard/index.d.ts +13 -10
  27. package/dist/dashboard/index.js +3 -26
  28. package/dist/dashboard/index.js.map +1 -1
  29. package/dist/dashboard/order.d.ts +22 -0
  30. package/dist/dashboard/order.js +55 -0
  31. package/dist/dashboard/order.js.map +1 -0
  32. package/dist/dashboard/pending-actions.d.ts +39 -10
  33. package/dist/dashboard/pending-actions.js +166 -36
  34. package/dist/dashboard/pending-actions.js.map +1 -1
  35. package/dist/dashboard/quick-jump.d.ts +2 -1
  36. package/dist/dashboard/quick-jump.js +7 -4
  37. package/dist/dashboard/quick-jump.js.map +1 -1
  38. package/dist/dashboard/session-actions.d.ts +4 -4
  39. package/dist/dashboard/session-actions.js +1 -1
  40. package/dist/dashboard/session-actions.js.map +1 -1
  41. package/dist/dashboard/session-registry.d.ts +4 -3
  42. package/dist/dashboard/session-registry.js +16 -50
  43. package/dist/dashboard/session-registry.js.map +1 -1
  44. package/dist/dashboard/state.d.ts +1 -1
  45. package/dist/dashboard/state.js.map +1 -1
  46. package/dist/dashboard/ui-state-store.d.ts +16 -1
  47. package/dist/dashboard/ui-state-store.js +73 -2
  48. package/dist/dashboard/ui-state-store.js.map +1 -1
  49. package/dist/debug-state.d.ts +97 -0
  50. package/dist/debug-state.js +541 -0
  51. package/dist/debug-state.js.map +1 -0
  52. package/dist/debug.d.ts +38 -0
  53. package/dist/debug.js +219 -15
  54. package/dist/debug.js.map +1 -1
  55. package/dist/default-plugins/gh-pr-context.d.ts +2 -1
  56. package/dist/default-plugins/gh-pr-context.js +17 -11
  57. package/dist/default-plugins/gh-pr-context.js.map +1 -1
  58. package/dist/default-plugins/transcript-length.js +15 -2
  59. package/dist/default-plugins/transcript-length.js.map +1 -1
  60. package/dist/fast-control.js +37 -19
  61. package/dist/fast-control.js.map +1 -1
  62. package/dist/http-client.js +31 -2
  63. package/dist/http-client.js.map +1 -1
  64. package/dist/local-ui-server.d.ts +22 -0
  65. package/dist/local-ui-server.js +186 -0
  66. package/dist/local-ui-server.js.map +1 -0
  67. package/dist/login-flow.d.ts +7 -0
  68. package/dist/login-flow.js +120 -0
  69. package/dist/login-flow.js.map +1 -0
  70. package/dist/main.js +824 -152
  71. package/dist/main.js.map +1 -1
  72. package/dist/managed-launch-env.js +14 -0
  73. package/dist/managed-launch-env.js.map +1 -1
  74. package/dist/metadata-server.d.ts +36 -36
  75. package/dist/metadata-server.js +638 -137
  76. package/dist/metadata-server.js.map +1 -1
  77. package/dist/metadata-store.d.ts +4 -1
  78. package/dist/metadata-store.js +30 -2
  79. package/dist/metadata-store.js.map +1 -1
  80. package/dist/multiplexer/agent-io-methods.d.ts +2 -10
  81. package/dist/multiplexer/agent-io-methods.js +12 -43
  82. package/dist/multiplexer/agent-io-methods.js.map +1 -1
  83. package/dist/multiplexer/archives.js +8 -9
  84. package/dist/multiplexer/archives.js.map +1 -1
  85. package/dist/multiplexer/dashboard-control.js +45 -13
  86. package/dist/multiplexer/dashboard-control.js.map +1 -1
  87. package/dist/multiplexer/dashboard-interaction.d.ts +8 -2
  88. package/dist/multiplexer/dashboard-interaction.js +187 -28
  89. package/dist/multiplexer/dashboard-interaction.js.map +1 -1
  90. package/dist/multiplexer/dashboard-model.d.ts +10 -3
  91. package/dist/multiplexer/dashboard-model.js +417 -35
  92. package/dist/multiplexer/dashboard-model.js.map +1 -1
  93. package/dist/multiplexer/dashboard-ops.d.ts +9 -7
  94. package/dist/multiplexer/dashboard-ops.js +178 -68
  95. package/dist/multiplexer/dashboard-ops.js.map +1 -1
  96. package/dist/multiplexer/dashboard-state-methods.d.ts +2 -1
  97. package/dist/multiplexer/dashboard-state-methods.js +3 -2
  98. package/dist/multiplexer/dashboard-state-methods.js.map +1 -1
  99. package/dist/multiplexer/dashboard-tail-methods.d.ts +22 -10
  100. package/dist/multiplexer/dashboard-tail-methods.js +164 -47
  101. package/dist/multiplexer/dashboard-tail-methods.js.map +1 -1
  102. package/dist/multiplexer/dashboard-view-methods.d.ts +1 -1
  103. package/dist/multiplexer/dashboard-view-methods.js +23 -8
  104. package/dist/multiplexer/dashboard-view-methods.js.map +1 -1
  105. package/dist/multiplexer/graveyard-view-model.d.ts +9 -1
  106. package/dist/multiplexer/graveyard-view-model.js +39 -0
  107. package/dist/multiplexer/graveyard-view-model.js.map +1 -1
  108. package/dist/multiplexer/index.d.ts +15 -12
  109. package/dist/multiplexer/index.js +64 -43
  110. package/dist/multiplexer/index.js.map +1 -1
  111. package/dist/multiplexer/notifications.js +107 -24
  112. package/dist/multiplexer/notifications.js.map +1 -1
  113. package/dist/multiplexer/persistence-methods.d.ts +31 -4
  114. package/dist/multiplexer/persistence-methods.js +304 -308
  115. package/dist/multiplexer/persistence-methods.js.map +1 -1
  116. package/dist/multiplexer/runtime-lifecycle-methods.d.ts +8 -10
  117. package/dist/multiplexer/runtime-lifecycle-methods.js +104 -86
  118. package/dist/multiplexer/runtime-lifecycle-methods.js.map +1 -1
  119. package/dist/multiplexer/runtime-state.d.ts +8 -10
  120. package/dist/multiplexer/runtime-state.js +82 -145
  121. package/dist/multiplexer/runtime-state.js.map +1 -1
  122. package/dist/multiplexer/runtime-sync.d.ts +2 -10
  123. package/dist/multiplexer/runtime-sync.js +3 -18
  124. package/dist/multiplexer/runtime-sync.js.map +1 -1
  125. package/dist/multiplexer/service-state-snapshot.d.ts +2 -4
  126. package/dist/multiplexer/service-state-snapshot.js +4 -51
  127. package/dist/multiplexer/service-state-snapshot.js.map +1 -1
  128. package/dist/multiplexer/services.d.ts +1 -0
  129. package/dist/multiplexer/services.js +55 -5
  130. package/dist/multiplexer/services.js.map +1 -1
  131. package/dist/multiplexer/session-capture.d.ts +1 -0
  132. package/dist/multiplexer/session-capture.js +24 -0
  133. package/dist/multiplexer/session-capture.js.map +1 -0
  134. package/dist/multiplexer/session-launch.d.ts +4 -1
  135. package/dist/multiplexer/session-launch.js +152 -63
  136. package/dist/multiplexer/session-launch.js.map +1 -1
  137. package/dist/multiplexer/session-runtime-core.d.ts +8 -20
  138. package/dist/multiplexer/session-runtime-core.js +40 -135
  139. package/dist/multiplexer/session-runtime-core.js.map +1 -1
  140. package/dist/multiplexer/subscreens.js +10 -3
  141. package/dist/multiplexer/subscreens.js.map +1 -1
  142. package/dist/multiplexer/worktree-graveyard.d.ts +0 -1
  143. package/dist/multiplexer/worktree-graveyard.js +15 -16
  144. package/dist/multiplexer/worktree-graveyard.js.map +1 -1
  145. package/dist/multiplexer/worktrees.js +96 -40
  146. package/dist/multiplexer/worktrees.js.map +1 -1
  147. package/dist/notification-context.js +8 -4
  148. package/dist/notification-context.js.map +1 -1
  149. package/dist/notifications.js +163 -101
  150. package/dist/notifications.js.map +1 -1
  151. package/dist/notify.d.ts +4 -0
  152. package/dist/notify.js +14 -0
  153. package/dist/notify.js.map +1 -1
  154. package/dist/paths.d.ts +32 -7
  155. package/dist/paths.js +82 -58
  156. package/dist/paths.js.map +1 -1
  157. package/dist/pending-actions.d.ts +5 -0
  158. package/dist/pending-actions.js +14 -0
  159. package/dist/pending-actions.js.map +1 -0
  160. package/dist/plugin-runtime.js +9 -2
  161. package/dist/plugin-runtime.js.map +1 -1
  162. package/dist/project-events.d.ts +1 -10
  163. package/dist/project-events.js +0 -10
  164. package/dist/project-events.js.map +1 -1
  165. package/dist/project-scanner.d.ts +2 -3
  166. package/dist/project-scanner.js +58 -129
  167. package/dist/project-scanner.js.map +1 -1
  168. package/dist/project-service-manifest.d.ts +1 -3
  169. package/dist/project-service-manifest.js +1 -3
  170. package/dist/project-service-manifest.js.map +1 -1
  171. package/dist/relay-client.d.ts +30 -0
  172. package/dist/relay-client.js +191 -0
  173. package/dist/relay-client.js.map +1 -0
  174. package/dist/remote-access.d.ts +16 -0
  175. package/dist/remote-access.js +91 -0
  176. package/dist/remote-access.js.map +1 -0
  177. package/dist/runtime-core/exchange-derived.d.ts +2 -0
  178. package/dist/runtime-core/exchange-derived.js +154 -0
  179. package/dist/runtime-core/exchange-derived.js.map +1 -0
  180. package/dist/runtime-core/exchange-import.d.ts +24 -0
  181. package/dist/runtime-core/exchange-import.js +318 -0
  182. package/dist/runtime-core/exchange-import.js.map +1 -0
  183. package/dist/runtime-core/exchange-store.d.ts +157 -0
  184. package/dist/runtime-core/exchange-store.js +453 -0
  185. package/dist/runtime-core/exchange-store.js.map +1 -0
  186. package/dist/runtime-core/topology-services.d.ts +38 -0
  187. package/dist/runtime-core/topology-services.js +171 -0
  188. package/dist/runtime-core/topology-services.js.map +1 -0
  189. package/dist/runtime-core/topology-sessions.d.ts +52 -0
  190. package/dist/runtime-core/topology-sessions.js +239 -0
  191. package/dist/runtime-core/topology-sessions.js.map +1 -0
  192. package/dist/runtime-core/topology-store.d.ts +171 -0
  193. package/dist/runtime-core/topology-store.js +420 -0
  194. package/dist/runtime-core/topology-store.js.map +1 -0
  195. package/dist/runtime-core/topology-worktrees.d.ts +60 -0
  196. package/dist/runtime-core/topology-worktrees.js +200 -0
  197. package/dist/runtime-core/topology-worktrees.js.map +1 -0
  198. package/dist/runtime-migration.d.ts +69 -0
  199. package/dist/runtime-migration.js +399 -0
  200. package/dist/runtime-migration.js.map +1 -0
  201. package/dist/session-bootstrap.d.ts +8 -6
  202. package/dist/session-bootstrap.js +51 -158
  203. package/dist/session-bootstrap.js.map +1 -1
  204. package/dist/session-runtime.d.ts +2 -0
  205. package/dist/session-runtime.js +1 -0
  206. package/dist/session-runtime.js.map +1 -1
  207. package/dist/session-semantics.d.ts +12 -4
  208. package/dist/session-semantics.js +14 -0
  209. package/dist/session-semantics.js.map +1 -1
  210. package/dist/shell-hooks.js +32 -10
  211. package/dist/shell-hooks.js.map +1 -1
  212. package/dist/shell-state.d.ts +2 -0
  213. package/dist/shell-state.js +26 -1
  214. package/dist/shell-state.js.map +1 -1
  215. package/dist/statusline-model.d.ts +10 -2
  216. package/dist/statusline-model.js +106 -30
  217. package/dist/statusline-model.js.map +1 -1
  218. package/dist/task-workflow.d.ts +6 -9
  219. package/dist/task-workflow.js +37 -84
  220. package/dist/task-workflow.js.map +1 -1
  221. package/dist/tasks.d.ts +6 -33
  222. package/dist/tasks.js +46 -88
  223. package/dist/tasks.js.map +1 -1
  224. package/dist/team.d.ts +29 -0
  225. package/dist/team.js +40 -0
  226. package/dist/team.js.map +1 -1
  227. package/dist/threads.d.ts +6 -35
  228. package/dist/threads.js +89 -98
  229. package/dist/threads.js.map +1 -1
  230. package/dist/tmux/inbox-popup.js +37 -15
  231. package/dist/tmux/inbox-popup.js.map +1 -1
  232. package/dist/tmux/runtime-manager.d.ts +3 -0
  233. package/dist/tmux/runtime-manager.js +21 -4
  234. package/dist/tmux/runtime-manager.js.map +1 -1
  235. package/dist/tmux/statusline.js +49 -9
  236. package/dist/tmux/statusline.js.map +1 -1
  237. package/dist/tmux/window-open.js +1 -2
  238. package/dist/tmux/window-open.js.map +1 -1
  239. package/dist/tool-output-watchers.d.ts +0 -18
  240. package/dist/tool-output-watchers.js +0 -322
  241. package/dist/tool-output-watchers.js.map +1 -1
  242. package/dist/tui/screens/dashboard-renderers.js +37 -25
  243. package/dist/tui/screens/dashboard-renderers.js.map +1 -1
  244. package/dist/tui/screens/overlay-renderers.d.ts +2 -0
  245. package/dist/tui/screens/overlay-renderers.js +37 -1
  246. package/dist/tui/screens/overlay-renderers.js.map +1 -1
  247. package/dist/tui/screens/subscreen-renderers.js +7 -0
  248. package/dist/tui/screens/subscreen-renderers.js.map +1 -1
  249. package/dist/worktree.js +17 -0
  250. package/dist/worktree.js.map +1 -1
  251. package/dist-ui/_expo/static/css/web-30453ede1678c16acb08b97e83e8646d.css +1 -0
  252. package/dist-ui/_expo/static/js/web/entry-477c745b2adc79367a4380ecf07d9ff6.js +14620 -0
  253. package/dist-ui/assets/assets/images/icon.a5413dcd2e811c9f2317d01a28118d8a.png +0 -0
  254. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  255. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  256. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  257. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  258. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  259. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  260. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  261. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  262. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  263. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  264. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  265. package/dist-ui/assets/node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  266. package/dist-ui/assets/node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  267. package/dist-ui/assets/node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  268. package/dist-ui/assets/node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  269. package/dist-ui/assets/node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  270. package/dist-ui/assets/node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  271. package/dist-ui/assets/node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  272. package/dist-ui/favicon.ico +0 -0
  273. package/dist-ui/index.html +38 -0
  274. package/dist-ui/metadata.json +1 -0
  275. package/package.json +31 -15
  276. package/dist/agent-message-parts.d.ts +0 -17
  277. package/dist/agent-message-parts.js +0 -31
  278. package/dist/agent-message-parts.js.map +0 -1
  279. package/dist/instance-directory.d.ts +0 -32
  280. package/dist/instance-directory.js +0 -82
  281. package/dist/instance-directory.js.map +0 -1
  282. package/dist/instance-registry.d.ts +0 -39
  283. package/dist/instance-registry.js +0 -208
  284. package/dist/instance-registry.js.map +0 -1
  285. package/dist/multiplexer/session-actions.d.ts +0 -40
  286. package/dist/multiplexer/session-actions.js +0 -110
  287. package/dist/multiplexer/session-actions.js.map +0 -1
  288. package/dist/orchestration-dispatcher.d.ts +0 -25
  289. package/dist/orchestration-dispatcher.js +0 -59
  290. package/dist/orchestration-dispatcher.js.map +0 -1
  291. package/dist/session-input-operations.d.ts +0 -19
  292. package/dist/session-input-operations.js +0 -46
  293. package/dist/session-input-operations.js.map +0 -1
  294. package/dist/session-message-history.d.ts +0 -27
  295. package/dist/session-message-history.js +0 -105
  296. package/dist/session-message-history.js.map +0 -1
  297. package/dist/task-dispatcher.d.ts +0 -64
  298. package/dist/task-dispatcher.js +0 -213
  299. package/dist/task-dispatcher.js.map +0 -1
package/dist/main.js CHANGED
@@ -5,18 +5,21 @@ import { homedir } from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { Multiplexer } from "./multiplexer/index.js";
7
7
  import { llmCompact } from "./context/compactor.js";
8
- import { initProject } from "./config.js";
9
- import { initPaths, getHistoryDir, getGraveyardPath, getStatePath, getContextDir, getProjectId, getProjectStateDirFor, } from "./paths.js";
8
+ import { initProject, loadConfig } from "./config.js";
9
+ import { initPaths, getHistoryDir, getContextDir, getProjectId, getRepoRoot, getDaemonLogPath, getProjectLogPath, getProjectStateDirFor, getRuntimeTopologyPath, } from "./paths.js";
10
10
  import { loadTeamConfig, saveTeamConfig, getDefaultTeamConfig } from "./team.js";
11
- import { createWorktree, findMainRepo, listWorktrees } from "./worktree.js";
11
+ import { findMainRepo, listWorktrees } from "./worktree.js";
12
12
  import { TmuxRuntimeManager } from "./tmux/runtime-manager.js";
13
13
  import { buildTmuxDoctorReport, renderTmuxDoctorReport, renderTmuxRepairResult, repairTmuxRuntime, } from "./tmux/doctor.js";
14
- import { loadMetadataEndpoint, resolveProjectServiceEndpoint as resolveStoredProjectServiceEndpoint, updateSessionMetadata, clearSessionLogs, removeMetadataEndpoint, } from "./metadata-store.js";
14
+ import { loadMetadataEndpoint, resolveProjectServiceEndpoint as resolveStoredProjectServiceEndpoint, updateSessionMetadata, clearSessionLogs, loadMetadataState, removeMetadataEndpoint, } from "./metadata-store.js";
15
+ import { contextualizeAlertInput, metadataDisplayContext } from "./alert-display.js";
15
16
  import { AgentTracker } from "./agent-tracker.js";
16
- import { AimuxDaemon, ensureDaemonRunning, ensureProjectService, loadDaemonInfo, loadDaemonState, projectServiceStatus, requestDaemonJson, stopDaemon, stopProjectService, } from "./daemon.js";
17
+ import { AimuxDaemon, ensureDaemonRunning, ensureProjectService, getDaemonHost, getDaemonPort, loadDaemonInfo, loadDaemonState, projectServiceStatus, requestDaemonJson, stopDaemon, stopProjectService, } from "./daemon.js";
17
18
  import { getProjectServiceManifest, manifestsMatch } from "./project-service-manifest.js";
18
19
  import { createThread, listThreadSummaries, markThreadSeen, readMessages, readThread, setThreadStatus, } from "./threads.js";
19
20
  import { sendDirectMessage, sendThreadMessage } from "./orchestration.js";
21
+ import { runLoginFlow } from "./login-flow.js";
22
+ import { clearCredentials, loadCredentials, setRemoteEnabled } from "./credentials.js";
20
23
  import { acceptHandoff, approveReview, acceptTask, assignTask, blockTask, completeHandoff, completeTask, reopenTask, requestTaskChanges, sendHandoff, } from "./orchestration-actions.js";
21
24
  import { readAllTasks, readTask } from "./tasks.js";
22
25
  import { clearNotifications, listNotifications, markNotificationsRead, upsertNotification, unreadNotificationCount, } from "./notifications.js";
@@ -25,11 +28,18 @@ import { parseClaudeHookPayload, summarizeClaudeNotification, summarizeClaudeSto
25
28
  import { requestJson } from "./http-client.js";
26
29
  import { runTmuxSwitcher } from "./tmux/switcher.js";
27
30
  import { runTmuxInboxPopup } from "./tmux/inbox-popup.js";
31
+ import { buildDebugStateReport, renderDebugStateReport } from "./debug-state.js";
28
32
  import { getDashboardCommandSpec } from "./dashboard/command-spec.js";
29
33
  import { findLiveDashboardTarget, openDashboardTarget, pruneDashboardArtifacts, resolveDashboardTarget, } from "./dashboard/targets.js";
30
34
  import { invalidateTmuxStatuslineArtifacts } from "./tmux/statusline-cache.js";
31
35
  import { loadStatusline, renderTmuxStatuslineFromData } from "./tmux/statusline.js";
32
36
  import { persistProjectRuntimeSnapshotsBeforeTmuxStop } from "./multiplexer/service-state-snapshot.js";
37
+ import { configureLogging, log, resolveLoggingRuntimeConfig } from "./debug.js";
38
+ import { createRuntimeTopologyStore } from "./runtime-core/topology-store.js";
39
+ import { listTopologySessionStates } from "./runtime-core/topology-sessions.js";
40
+ import { listTopologyWorktreeGraveyard, listTopologyWorktreeGraveyardPaths, } from "./runtime-core/topology-worktrees.js";
41
+ import { buildRuntimeMigrationReport, importRuntimeMigration, renderRuntimeMigrationImportResult, renderRuntimeMigrationReport, renderRuntimeMigrationRollbackResult, rollbackRuntimeMigration, } from "./runtime-migration.js";
42
+ import { DEFAULT_LOCAL_UI_HOST, DEFAULT_LOCAL_UI_PORT, openUrlInBrowser, startLocalUiServer, } from "./local-ui-server.js";
33
43
  const program = new Command();
34
44
  class ProjectServiceVersionError extends Error {
35
45
  projectRoot;
@@ -61,6 +71,7 @@ function renderProjectServiceVersionHelp(error) {
61
71
  }
62
72
  async function restartStaleControlPlane(projectRoot) {
63
73
  console.error(`aimux: restarting stale daemon-managed control plane for ${projectRoot}...`);
74
+ log.warn("restarting stale control plane", "runtime", { projectRoot });
64
75
  await stopDaemon();
65
76
  removeMetadataEndpoint(projectRoot);
66
77
  await ensureDaemonRunning();
@@ -94,9 +105,21 @@ async function waitForVerifiedProjectService(projectRoot, opts) {
94
105
  const health = await fetchProjectServiceHealth(endpoint);
95
106
  lastServiceInfo = health.serviceInfo ?? null;
96
107
  if (manifestsMatch(expected, health.serviceInfo)) {
108
+ log.info("project service verified", "runtime", {
109
+ projectRoot,
110
+ endpoint,
111
+ pid: health.pid,
112
+ elapsedMs: Date.now() - startedAt,
113
+ });
97
114
  return { endpoint, health };
98
115
  }
99
116
  lastError = `project service manifest mismatch: expected ${JSON.stringify(expected)} actual ${JSON.stringify(health.serviceInfo ?? null)}`;
117
+ log.warn("project service manifest mismatch", "runtime", {
118
+ projectRoot,
119
+ endpoint,
120
+ expected,
121
+ actual: health.serviceInfo ?? null,
122
+ });
100
123
  }
101
124
  catch (error) {
102
125
  lastError = error instanceof Error ? error.message : String(error);
@@ -106,6 +129,11 @@ async function waitForVerifiedProjectService(projectRoot, opts) {
106
129
  lastError.includes("ECONNRESET") ||
107
130
  lastError.includes("socket hang up"))) {
108
131
  respawnAttempted = true;
132
+ log.warn("respawning project service after connection failure", "runtime", {
133
+ projectRoot,
134
+ endpoint,
135
+ error: lastError,
136
+ });
109
137
  removeMetadataEndpoint(projectRoot);
110
138
  await ensureProjectService(projectRoot);
111
139
  }
@@ -118,6 +146,7 @@ async function waitForVerifiedProjectService(projectRoot, opts) {
118
146
  }
119
147
  else if (!respawnAttempted && Date.now() - missingEndpointSince >= 1000) {
120
148
  respawnAttempted = true;
149
+ log.warn("respawning project service after missing endpoint", "runtime", { projectRoot });
121
150
  await stopProjectService(projectRoot);
122
151
  removeMetadataEndpoint(projectRoot);
123
152
  await ensureProjectService(projectRoot);
@@ -160,7 +189,7 @@ function rewriteLocalStatuslineArtifacts(projectRoot, tmux, dashboardSessionName
160
189
  if (dashboardSessionName) {
161
190
  writeStatusFile(`bottom-dashboard-${dashboardSessionName}.txt`, dashboardBottom);
162
191
  }
163
- for (const entry of data.sessions ?? []) {
192
+ for (const entry of [...(data.sessions ?? []), ...(data.teammates ?? [])]) {
164
193
  if (!entry.tmuxWindowId)
165
194
  continue;
166
195
  const renderOptions = {
@@ -216,34 +245,74 @@ async function postProjectServiceJsonOrLocal(path, body, fallback) {
216
245
  return fallback();
217
246
  }
218
247
  }
248
+ async function getProjectServiceJsonOrLocal(path, fallback) {
249
+ try {
250
+ return await getProjectServiceJson(path);
251
+ }
252
+ catch {
253
+ return fallback();
254
+ }
255
+ }
219
256
  function exitAfterOpen() {
220
257
  process.exit(0);
221
258
  }
222
259
  async function postLiveProjectServiceJsonOrLocal(projectRoot, path, body, fallback) {
260
+ let endpoint;
223
261
  try {
224
- const endpoint = await resolveProjectServiceEndpoint(projectRoot);
225
- if (!endpoint) {
226
- return fallback();
227
- }
228
- const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}${path}`, {
229
- method: "POST",
230
- headers: { "content-type": "application/json" },
231
- body,
232
- });
233
- if (status < 200 || status >= 300 || json?.ok === false) {
234
- throw new Error(json?.error || `request failed: ${status}`);
235
- }
236
- return json;
262
+ endpoint = await resolveProjectServiceEndpoint(projectRoot);
237
263
  }
238
264
  catch {
239
265
  return fallback();
240
266
  }
267
+ if (!endpoint) {
268
+ return fallback();
269
+ }
270
+ const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}${path}`, {
271
+ method: "POST",
272
+ headers: { "content-type": "application/json" },
273
+ body,
274
+ });
275
+ if (status === 404 || status === 405 || status === 501) {
276
+ return fallback();
277
+ }
278
+ if (status < 200 || status >= 300 || json?.ok === false) {
279
+ throw new Error(json?.error || `request failed: ${status}`);
280
+ }
281
+ return json;
282
+ }
283
+ async function getLiveProjectServiceJsonOrLocal(projectRoot, path, fallback) {
284
+ let endpoint;
285
+ try {
286
+ endpoint = await resolveProjectServiceEndpoint(projectRoot);
287
+ }
288
+ catch {
289
+ return fallback();
290
+ }
291
+ if (!endpoint) {
292
+ return fallback();
293
+ }
294
+ let status;
295
+ let json;
296
+ try {
297
+ ({ status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}${path}`, {
298
+ method: "GET",
299
+ }));
300
+ }
301
+ catch {
302
+ return fallback();
303
+ }
304
+ if (status === 404 || status === 405 || status === 501) {
305
+ return fallback();
306
+ }
307
+ if (status < 200 || status >= 300 || json?.ok === false) {
308
+ throw new Error(json?.error || `request failed: ${status}`);
309
+ }
310
+ return json;
241
311
  }
242
312
  async function resolveClaudeHookSessionId(explicitSessionId, payloadSessionId) {
243
313
  if (!payloadSessionId)
244
314
  return explicitSessionId;
245
- const state = Multiplexer.loadState();
246
- const match = state?.sessions.find((session) => session.backendSessionId === payloadSessionId);
315
+ const match = listTopologySessionStates().find((session) => session.backendSessionId === payloadSessionId);
247
316
  return match?.id ?? explicitSessionId;
248
317
  }
249
318
  async function resolveProjectServiceEndpoint(projectRoot = resolveProjectRoot(process.cwd())) {
@@ -371,20 +440,101 @@ function ensureTmuxAvailable(tmux) {
371
440
  process.exit(1);
372
441
  }
373
442
  }
443
+ function commandPath(command) {
444
+ const names = [];
445
+ let current = command;
446
+ while (current) {
447
+ const name = current.name();
448
+ if (name)
449
+ names.unshift(name);
450
+ current = current.parent ?? null;
451
+ }
452
+ return names;
453
+ }
454
+ function loggingProcessKind(command) {
455
+ const names = commandPath(command);
456
+ if (names.at(-1) === "__project-service-internal")
457
+ return "project-service";
458
+ if (names.at(-2) === "daemon" && names.at(-1) === "run")
459
+ return "daemon";
460
+ return "cli";
461
+ }
462
+ function configureLoggingForCommand(command) {
463
+ const processKind = loggingProcessKind(command);
464
+ const config = loadConfig();
465
+ const path = processKind === "daemon" ? getDaemonLogPath() : getProjectLogPath();
466
+ const cli = program.opts();
467
+ const resolved = resolveLoggingRuntimeConfig({
468
+ config: config.logging,
469
+ env: process.env,
470
+ cli,
471
+ path,
472
+ processKind,
473
+ projectId: getProjectId(),
474
+ projectRoot: getRepoRoot(),
475
+ });
476
+ configureLogging(resolved);
477
+ log.info("logging configured", "logging", {
478
+ path: resolved.path,
479
+ level: resolved.level,
480
+ categories: resolved.categories,
481
+ });
482
+ }
483
+ function parseLineCount(value) {
484
+ const parsed = Number.parseInt(value ?? "80", 10);
485
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 80;
486
+ }
487
+ function parsePortOption(value, fallback) {
488
+ const parsed = Number.parseInt(value ?? String(fallback), 10);
489
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535) {
490
+ throw new Error(`Port must be an integer between 1 and 65535, got ${value}`);
491
+ }
492
+ return parsed;
493
+ }
494
+ function selectedLogPath(opts) {
495
+ return opts.daemon ? getDaemonLogPath() : getProjectLogPath();
496
+ }
497
+ function readLastLogLines(path, lines) {
498
+ if (!existsSync(path))
499
+ return "";
500
+ const content = readFileSync(path, "utf-8");
501
+ const allLines = content.split(/\r?\n/);
502
+ if (allLines.at(-1) === "")
503
+ allLines.pop();
504
+ return allLines.slice(-lines).join("\n");
505
+ }
506
+ const pkgJsonPath = pathJoin(pathDirname(fileURLToPath(import.meta.url)), "..", "package.json");
507
+ const pkgVersion = JSON.parse(readFileSync(pkgJsonPath, "utf8")).version;
374
508
  program
375
509
  .name("aimux")
376
510
  .description("Native CLI agent multiplexer")
377
- .version("0.1.0")
511
+ .version(pkgVersion)
378
512
  .argument("[tool]", "Tool to run (e.g. claude, codex, aider)")
379
513
  .argument("[args...]", "Arguments to pass to the tool")
380
514
  .option("--resume", "Resume previous sessions using native tool resume")
381
515
  .option("--restore", "Start fresh sessions with injected history context")
382
516
  .option("--tmux-dashboard-internal", "Internal tmux dashboard entrypoint")
517
+ .option("--debug", "Enable debug logging for this process")
518
+ .option("--trace", "Enable trace logging for this process")
519
+ .option("--log-level <level>", "Enable logging at level: error|warn|info|debug|trace")
520
+ .option("--log-category <categories>", "Comma-separated log categories to include")
383
521
  .hook("preAction", async (_thisCommand, actionCommand) => {
522
+ const names = commandPath(actionCommand);
523
+ const isMigrationAudit = names.at(-2) === "migration" && names.at(-1) === "audit";
524
+ if (isMigrationAudit) {
525
+ return;
526
+ }
384
527
  const opts = typeof actionCommand?.opts === "function" ? actionCommand.opts() : {};
385
- const requestedProject = typeof opts.project === "string" ? opts.project : undefined;
528
+ const requestedProject = typeof opts.project === "string"
529
+ ? opts.project
530
+ : typeof opts.projectRoot === "string"
531
+ ? opts.projectRoot
532
+ : typeof opts["project-root"] === "string"
533
+ ? opts["project-root"]
534
+ : undefined;
386
535
  const projectRoot = requestedProject ? resolveProjectRoot(pathResolve(requestedProject)) : undefined;
387
536
  await initPaths(projectRoot);
537
+ configureLoggingForCommand(actionCommand);
388
538
  })
389
539
  .action(async (tool, args, opts) => {
390
540
  const originalCwd = process.cwd();
@@ -445,11 +595,19 @@ program
445
595
  process.on("SIGTERM", shutdown);
446
596
  process.on("uncaughtException", (err) => {
447
597
  cleanupAll();
598
+ log.error("uncaught exception", "runtime", {
599
+ error: err instanceof Error ? err.message : String(err),
600
+ stack: err instanceof Error ? err.stack : undefined,
601
+ });
448
602
  console.error(err);
449
603
  process.exit(1);
450
604
  });
451
605
  process.on("unhandledRejection", (reason) => {
452
606
  cleanupAll();
607
+ log.error("unhandled rejection", "runtime", {
608
+ error: reason instanceof Error ? reason.message : String(reason),
609
+ stack: reason instanceof Error ? reason.stack : undefined,
610
+ });
453
611
  console.error(reason);
454
612
  process.exit(1);
455
613
  });
@@ -531,8 +689,11 @@ program
531
689
  try {
532
690
  if (sessionId) {
533
691
  const projectRoot = await prepareProjectContext(opts.project);
534
- const mux = new Multiplexer();
535
- const result = await mux.stopAgent(sessionId);
692
+ await ensureDaemonProjectReady(projectRoot);
693
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/agents/stop", { sessionId }, () => {
694
+ const mux = new Multiplexer();
695
+ return mux.stopAgent(sessionId);
696
+ });
536
697
  if (opts.json) {
537
698
  console.log(JSON.stringify({
538
699
  ok: true,
@@ -555,6 +716,7 @@ program
555
716
  projectServiceStopped: result.projectServiceStopped,
556
717
  tmuxSessionsKilled: result.tmuxSessionsKilled,
557
718
  }, null, 2));
719
+ process.exitCode = 1;
558
720
  return;
559
721
  }
560
722
  console.log(`Stopped project runtime for ${projectRoot}`);
@@ -606,6 +768,47 @@ program
606
768
  const hostCmd = program
607
769
  .command("host")
608
770
  .description("Advanced compatibility wrappers for legacy daemon-managed project services");
771
+ program
772
+ .command("ui")
773
+ .description("Run the first-party local web UI from the built app bundle")
774
+ .option("--host <host>", "Loopback host to bind", DEFAULT_LOCAL_UI_HOST)
775
+ .option("--port <port>", "Local UI port", String(DEFAULT_LOCAL_UI_PORT))
776
+ .option("--daemon-url <url>", "Daemon URL for the UI to call")
777
+ .option("--no-daemon", "Do not ensure the local daemon before serving")
778
+ .option("--open", "Open the UI in the default browser")
779
+ .action(async (opts) => {
780
+ try {
781
+ const shouldEnsureDaemon = opts.daemon !== false;
782
+ const daemonInfo = shouldEnsureDaemon ? await ensureDaemonRunning() : null;
783
+ const daemonUrl = opts.daemonUrl?.trim() || `http://${getDaemonHost()}:${daemonInfo?.port ?? getDaemonPort()}`;
784
+ const server = await startLocalUiServer({
785
+ host: opts.host,
786
+ port: parsePortOption(opts.port, DEFAULT_LOCAL_UI_PORT),
787
+ config: {
788
+ connectionMode: "local",
789
+ daemonUrl,
790
+ },
791
+ });
792
+ console.log(`aimux UI: ${server.url}`);
793
+ console.log(`Daemon: ${daemonUrl}`);
794
+ console.log("Press Ctrl-C to stop.");
795
+ if (opts.open) {
796
+ openUrlInBrowser(server.url);
797
+ }
798
+ const shutdown = async () => {
799
+ await server.close();
800
+ process.exit(0);
801
+ };
802
+ process.on("SIGINT", () => void shutdown());
803
+ process.on("SIGTERM", () => void shutdown());
804
+ await new Promise(() => { });
805
+ }
806
+ catch (err) {
807
+ const msg = err instanceof Error ? err.message : String(err);
808
+ console.error(`Error: ${msg}`);
809
+ process.exit(1);
810
+ }
811
+ });
609
812
  program
610
813
  .command("serve")
611
814
  .description("Advanced: ensure the legacy daemon-backed project control service is running")
@@ -718,27 +921,22 @@ hostCmd
718
921
  console.log(`Restarted project service for ${dashboardSession.sessionName}`);
719
922
  });
720
923
  hostCmd
721
- .command("agent-send")
722
- .description("Send raw input to a running agent session over the project HTTP service")
723
- .argument("<sessionId>", "Agent session ID")
724
- .argument("[data...]", "Input to send")
725
- .option("--stdin", "Read the full input payload from stdin")
726
- .option("--submit", "Submit after writing the input")
727
- .action(async (sessionId, data, opts) => {
924
+ .command("topology")
925
+ .description("Show the runtime topology YAML path or parsed contents")
926
+ .option("--json", "Emit parsed topology JSON")
927
+ .option("--raw", "Print raw YAML contents")
928
+ .action(async (opts) => {
728
929
  await initPaths();
729
- const payload = opts.stdin === true ? await readAllStdin() : data.join(" ");
730
- if (!payload) {
731
- throw new Error("input data is required");
732
- }
733
- const result = await postProjectServiceJson("/agents/input", {
734
- sessionId,
735
- data: payload,
736
- submit: opts.submit === true,
737
- });
738
- if (result.accepted === false) {
739
- throw new Error(result.error || `agent input failed for ${result.sessionId}`);
930
+ const path = getRuntimeTopologyPath();
931
+ if (opts.json) {
932
+ console.log(JSON.stringify(createRuntimeTopologyStore(path).read(), null, 2));
933
+ return;
740
934
  }
741
- console.log(`sent input to ${result.sessionId}`);
935
+ if (opts.raw) {
936
+ console.log(readFileSync(path, "utf-8"));
937
+ return;
938
+ }
939
+ console.log(path);
742
940
  });
743
941
  hostCmd
744
942
  .command("agent-read")
@@ -928,9 +1126,20 @@ daemonCmd
928
1126
  .action(async (opts) => {
929
1127
  const info = loadDaemonInfo();
930
1128
  const state = loadDaemonState();
1129
+ let relay = { status: "off" };
1130
+ if (info) {
1131
+ try {
1132
+ const result = await requestDaemonJson("/relay/status");
1133
+ relay = result.relay;
1134
+ }
1135
+ catch {
1136
+ // Relay status unavailable — leave as off.
1137
+ }
1138
+ }
931
1139
  const payload = {
932
1140
  daemon: info,
933
1141
  projects: Object.values(state.projects),
1142
+ relay,
934
1143
  };
935
1144
  if (opts.json) {
936
1145
  console.log(JSON.stringify(payload, null, 2));
@@ -942,6 +1151,13 @@ daemonCmd
942
1151
  }
943
1152
  console.log(`Daemon pid=${info.pid} port=${info.port}`);
944
1153
  console.log(`Managed projects: ${Object.keys(state.projects).length}`);
1154
+ const r = relay;
1155
+ if (r.status && r.status !== "off") {
1156
+ console.log(`Relay: ${r.status}${r.relayUrl ? ` (${r.relayUrl})` : ""}`);
1157
+ }
1158
+ else {
1159
+ console.log("Relay: off");
1160
+ }
945
1161
  });
946
1162
  daemonCmd
947
1163
  .command("projects")
@@ -1001,11 +1217,19 @@ program
1001
1217
  process.on("SIGTERM", shutdown);
1002
1218
  process.on("uncaughtException", (err) => {
1003
1219
  cleanupAll();
1220
+ log.error("project service uncaught exception", "runtime", {
1221
+ error: err instanceof Error ? err.message : String(err),
1222
+ stack: err instanceof Error ? err.stack : undefined,
1223
+ });
1004
1224
  console.error(err);
1005
1225
  process.exit(1);
1006
1226
  });
1007
1227
  process.on("unhandledRejection", (reason) => {
1008
1228
  cleanupAll();
1229
+ log.error("project service unhandled rejection", "runtime", {
1230
+ error: reason instanceof Error ? reason.message : String(reason),
1231
+ stack: reason instanceof Error ? reason.stack : undefined,
1232
+ });
1009
1233
  console.error(reason);
1010
1234
  process.exit(1);
1011
1235
  });
@@ -1039,15 +1263,8 @@ projectsCmd
1039
1263
  return;
1040
1264
  }
1041
1265
  for (const project of projects) {
1042
- const liveBadge = project.sessions.some((session) => session.status !== "offline") ? "live" : "idle";
1266
+ const liveBadge = project.serviceAlive ? "live" : "idle";
1043
1267
  console.log(`${project.name} ${liveBadge} ${project.path}`);
1044
- if (project.sessions.length === 0)
1045
- continue;
1046
- for (const session of project.sessions) {
1047
- const label = session.label ? ` ${session.label}` : "";
1048
- const headline = session.headline ? ` - ${session.headline}` : "";
1049
- console.log(` ${session.id} ${session.tool} ${session.status}${label}${headline}`);
1050
- }
1051
1268
  }
1052
1269
  });
1053
1270
  program
@@ -1073,6 +1290,166 @@ program
1073
1290
  llmCompact(sessionIds);
1074
1291
  console.log(`Done. Summary written to ${getContextDir()}/summary.md`);
1075
1292
  });
1293
+ program
1294
+ .command("login")
1295
+ .description("Sign in to enable remote access via aimux.app")
1296
+ .option("--web-app-url <url>", "Override the web app URL")
1297
+ // No --relay-url here: the token is minted by whichever relay the web app
1298
+ // points at, so a CLI override would just store a relay URL that rejects
1299
+ // the resulting token (different RELAY_TOKEN_SECRET).
1300
+ .action(async (opts) => {
1301
+ try {
1302
+ const { userId } = await runLoginFlow({ webAppUrl: opts.webAppUrl });
1303
+ let relayStatus = null;
1304
+ let relayError = null;
1305
+ if (loadDaemonInfo()) {
1306
+ try {
1307
+ const result = await requestDaemonJson("/relay/enable", { method: "POST" });
1308
+ const relay = result.relay;
1309
+ relayStatus = relay.status ?? "unknown";
1310
+ relayError = relay.lastError ?? null;
1311
+ }
1312
+ catch (err) {
1313
+ relayError = err instanceof Error ? err.message : String(err);
1314
+ }
1315
+ }
1316
+ console.log(`\n✓ Logged in as ${userId}`);
1317
+ if (relayStatus) {
1318
+ console.log(`Remote access is enabled (connection: ${relayStatus}).`);
1319
+ if (relayError)
1320
+ console.log(`Last error: ${relayError}`);
1321
+ }
1322
+ else {
1323
+ console.log("Remote access is enabled. The daemon will connect on next start.");
1324
+ if (relayError)
1325
+ console.log(`Daemon refresh failed: ${relayError}`);
1326
+ }
1327
+ }
1328
+ catch (err) {
1329
+ console.error(`Login failed: ${err instanceof Error ? err.message : String(err)}`);
1330
+ process.exit(1);
1331
+ }
1332
+ });
1333
+ program
1334
+ .command("logout")
1335
+ .description("Clear stored credentials and disable remote access")
1336
+ .action(async () => {
1337
+ // If the daemon is running it already has the credential loaded into
1338
+ // memory; tell it to disconnect before we yank the file so the running
1339
+ // process stops talking to the relay immediately (best-effort — we
1340
+ // ignore failures since the daemon may not be up).
1341
+ if (loadDaemonInfo()) {
1342
+ try {
1343
+ await requestDaemonJson("/relay/disable", { method: "POST" });
1344
+ }
1345
+ catch {
1346
+ // daemon offline or refused; the file removal below still kills
1347
+ // future startup, so this isn't fatal.
1348
+ }
1349
+ }
1350
+ const result = clearCredentials();
1351
+ if (result === "cleared")
1352
+ console.log("✓ Logged out. Remote access disabled.");
1353
+ else if (result === "none")
1354
+ console.log("Not logged in.");
1355
+ else {
1356
+ console.error("Failed to remove credentials file — check permissions.");
1357
+ process.exitCode = 1;
1358
+ }
1359
+ });
1360
+ program
1361
+ .command("whoami")
1362
+ .description("Show the current remote-access login status")
1363
+ .option("--json", "Emit JSON")
1364
+ .action((opts) => {
1365
+ const creds = loadCredentials();
1366
+ if (opts.json) {
1367
+ console.log(JSON.stringify(creds
1368
+ ? { loggedIn: true, userId: creds.userId, relayUrl: creds.relayUrl, remoteEnabled: creds.remoteEnabled }
1369
+ : { loggedIn: false }, null, 2));
1370
+ return;
1371
+ }
1372
+ if (!creds) {
1373
+ console.log("Not logged in. Run `aimux login` to enable remote access.");
1374
+ return;
1375
+ }
1376
+ console.log(`Logged in as ${creds.userId}`);
1377
+ console.log(`Relay: ${creds.relayUrl}`);
1378
+ console.log(`Remote access: ${creds.remoteEnabled ? "enabled" : "disabled"}`);
1379
+ });
1380
+ const remoteCmd = program.command("remote").description("Manage remote access via the relay");
1381
+ const securityCmd = program.command("security").description("Manage aimux security controls");
1382
+ remoteCmd
1383
+ .command("status")
1384
+ .description("Show relay connection status")
1385
+ .option("--json", "Emit JSON")
1386
+ .action(async (opts) => {
1387
+ const creds = loadCredentials();
1388
+ let relay = { status: "off" };
1389
+ if (loadDaemonInfo()) {
1390
+ try {
1391
+ const result = await requestDaemonJson("/relay/status");
1392
+ relay = result.relay;
1393
+ }
1394
+ catch {
1395
+ // Daemon is not reachable — fall back to credential state.
1396
+ }
1397
+ }
1398
+ if (opts.json) {
1399
+ console.log(JSON.stringify({ loggedIn: Boolean(creds), relay }, null, 2));
1400
+ return;
1401
+ }
1402
+ if (!creds) {
1403
+ console.log("Not logged in. Run `aimux login` to enable remote access.");
1404
+ return;
1405
+ }
1406
+ const r = relay;
1407
+ console.log(`Remote access: ${creds.remoteEnabled ? "enabled" : "disabled"}`);
1408
+ console.log(`Relay: ${creds.relayUrl}`);
1409
+ console.log(`Connection: ${r.status ?? "unknown"}`);
1410
+ if (r.lastError)
1411
+ console.log(`Last error: ${r.lastError}`);
1412
+ });
1413
+ remoteCmd
1414
+ .command("enable")
1415
+ .description("Enable remote access and connect to the relay")
1416
+ .action(async () => {
1417
+ if (!loadCredentials()) {
1418
+ console.error("Not logged in. Run `aimux login` first.");
1419
+ process.exit(1);
1420
+ }
1421
+ await ensureDaemonRunning();
1422
+ const result = await requestDaemonJson("/relay/enable", { method: "POST" });
1423
+ const r = result.relay;
1424
+ console.log(`✓ Remote access enabled (connection: ${r.status ?? "unknown"})`);
1425
+ });
1426
+ remoteCmd
1427
+ .command("disable")
1428
+ .description("Disable remote access and disconnect from the relay")
1429
+ .action(async () => {
1430
+ if (loadDaemonInfo()) {
1431
+ await requestDaemonJson("/relay/disable", { method: "POST" });
1432
+ console.log("✓ Remote access disabled. Daemon disconnected from relay.");
1433
+ return;
1434
+ }
1435
+ setRemoteEnabled(false);
1436
+ console.log("✓ Remote access disabled.");
1437
+ });
1438
+ securityCmd
1439
+ .command("unlock")
1440
+ .description("Clear relay security lockdown after re-authenticating")
1441
+ .option("--web-app-url <url>", "Override the web app URL")
1442
+ .action(async (opts) => {
1443
+ try {
1444
+ const { userId } = await runLoginFlow({ webAppUrl: opts.webAppUrl, action: "security-unlock" });
1445
+ console.log(`\n✓ Security unlocked for ${userId}`);
1446
+ console.log("Remote access is enabled with a fresh daemon token. Restart the daemon to reconnect immediately.");
1447
+ }
1448
+ catch (err) {
1449
+ console.error(`Security unlock failed: ${err instanceof Error ? err.message : String(err)}`);
1450
+ process.exit(1);
1451
+ }
1452
+ });
1076
1453
  async function prepareProjectContext(requestedProject) {
1077
1454
  const requestedPath = pathResolve(requestedProject ?? process.cwd());
1078
1455
  const projectRoot = resolveProjectRoot(requestedPath);
@@ -1080,9 +1457,13 @@ async function prepareProjectContext(requestedProject) {
1080
1457
  process.chdir(projectRoot);
1081
1458
  return projectRoot;
1082
1459
  }
1083
- function printWorktrees(projectRoot) {
1460
+ function listVisibleLocalWorktrees(projectRoot) {
1461
+ const graveyardPaths = listTopologyWorktreeGraveyardPaths();
1462
+ return listWorktrees(projectRoot).filter((worktree) => !graveyardPaths.has(worktree.path));
1463
+ }
1464
+ function printWorktrees(projectRoot, worktreesInput) {
1084
1465
  try {
1085
- const worktrees = listWorktrees(projectRoot);
1466
+ const worktrees = worktreesInput ?? listWorktrees(projectRoot);
1086
1467
  if (worktrees.length === 0) {
1087
1468
  console.log("No worktrees found.");
1088
1469
  return;
@@ -1099,9 +1480,55 @@ function printWorktrees(projectRoot) {
1099
1480
  process.exit(1);
1100
1481
  }
1101
1482
  }
1483
+ function printGraveyard(input) {
1484
+ const entries = Array.isArray(input.entries) ? input.entries : [];
1485
+ const worktrees = Array.isArray(input.worktrees) ? input.worktrees : [];
1486
+ if (entries.length === 0 && worktrees.length === 0) {
1487
+ console.log("Graveyard is empty.");
1488
+ return;
1489
+ }
1490
+ if (worktrees.length > 0) {
1491
+ console.log("Worktrees");
1492
+ console.log("Name".padEnd(30) + "Branch".padEnd(35) + "Path");
1493
+ console.log("-".repeat(95));
1494
+ for (const worktree of worktrees) {
1495
+ console.log(String(worktree.name ?? "?").padEnd(30) +
1496
+ String(worktree.branch ?? "").padEnd(35) +
1497
+ String(worktree.path ?? "?"));
1498
+ }
1499
+ }
1500
+ if (entries.length > 0) {
1501
+ if (worktrees.length > 0)
1502
+ console.log("");
1503
+ console.log("Agents");
1504
+ console.log("ID".padEnd(25) + "Tool".padEnd(15) + "Backend Session ID");
1505
+ console.log("-".repeat(70));
1506
+ for (const session of entries) {
1507
+ console.log(String(session.id ?? "?").padEnd(25) +
1508
+ String(session.command ?? session.tool ?? "?").padEnd(15) +
1509
+ String(session.backendSessionId ?? "(none)"));
1510
+ }
1511
+ }
1512
+ }
1102
1513
  const worktreeCmd = program.command("worktree").description("Manage git worktrees");
1103
- worktreeCmd.action(() => {
1104
- printWorktrees();
1514
+ async function ensureDaemonProjectReadyForFallback(projectRoot) {
1515
+ try {
1516
+ await ensureDaemonProjectReady(projectRoot);
1517
+ }
1518
+ catch (err) {
1519
+ if (err instanceof ProjectServiceVersionError) {
1520
+ throw err;
1521
+ }
1522
+ }
1523
+ }
1524
+ worktreeCmd.action(async () => {
1525
+ const projectRoot = await prepareProjectContext();
1526
+ await ensureDaemonProjectReadyForFallback(projectRoot);
1527
+ const result = await getLiveProjectServiceJsonOrLocal(projectRoot, "/worktrees", () => ({
1528
+ ok: true,
1529
+ worktrees: listVisibleLocalWorktrees(projectRoot),
1530
+ }));
1531
+ printWorktrees(projectRoot, result.worktrees ?? []);
1105
1532
  });
1106
1533
  const threadCmd = program.command("thread").description("Inspect and manage orchestration threads");
1107
1534
  program
@@ -1109,8 +1536,9 @@ program
1109
1536
  .description("Alias for thread list")
1110
1537
  .option("--session <sessionId>", "Filter to threads involving a session")
1111
1538
  .option("--json", "Emit JSON")
1112
- .action((opts) => {
1113
- const summaries = listThreadSummaries(opts.session);
1539
+ .action(async (opts) => {
1540
+ const query = opts.session ? `?session=${encodeURIComponent(opts.session)}` : "";
1541
+ const summaries = await getProjectServiceJsonOrLocal(`/threads${query}`, () => listThreadSummaries(opts.session));
1114
1542
  if (opts.json) {
1115
1543
  console.log(JSON.stringify(summaries, null, 2));
1116
1544
  return;
@@ -1134,8 +1562,9 @@ threadCmd
1134
1562
  .description("List orchestration threads")
1135
1563
  .option("--session <sessionId>", "Filter to threads involving a session")
1136
1564
  .option("--json", "Emit JSON")
1137
- .action((opts) => {
1138
- const summaries = listThreadSummaries(opts.session);
1565
+ .action(async (opts) => {
1566
+ const query = opts.session ? `?session=${encodeURIComponent(opts.session)}` : "";
1567
+ const summaries = await getProjectServiceJsonOrLocal(`/threads${query}`, () => listThreadSummaries(opts.session));
1139
1568
  if (opts.json) {
1140
1569
  console.log(JSON.stringify(summaries, null, 2));
1141
1570
  return;
@@ -1159,13 +1588,18 @@ threadCmd
1159
1588
  .description("Show a thread and its messages")
1160
1589
  .argument("<threadId>")
1161
1590
  .option("--json", "Emit JSON")
1162
- .action((threadId, opts) => {
1163
- const thread = readThread(threadId);
1164
- if (!thread) {
1591
+ .action(async (threadId, opts) => {
1592
+ const detail = await getProjectServiceJsonOrLocal(`/threads/${encodeURIComponent(threadId)}`, () => {
1593
+ const thread = readThread(threadId);
1594
+ if (!thread)
1595
+ return null;
1596
+ return { thread, messages: readMessages(threadId) };
1597
+ });
1598
+ if (!detail?.thread) {
1165
1599
  console.error(`aimux: thread not found: ${threadId}`);
1166
1600
  process.exit(1);
1167
1601
  }
1168
- const messages = readMessages(threadId);
1602
+ const { thread, messages } = detail;
1169
1603
  if (opts.json) {
1170
1604
  console.log(JSON.stringify({ thread, messages }, null, 2));
1171
1605
  return;
@@ -1191,18 +1625,25 @@ threadCmd
1191
1625
  .requiredOption("--from <sessionId>", "Creating session")
1192
1626
  .requiredOption("--participants <ids>", "Comma-separated participant session ids")
1193
1627
  .option("--kind <kind>", "conversation|task|review|handoff|user", "conversation")
1194
- .action((opts) => {
1628
+ .action(async (opts) => {
1195
1629
  const participants = opts.participants
1196
1630
  .split(",")
1197
1631
  .map((value) => value.trim())
1198
1632
  .filter(Boolean);
1199
- const thread = createThread({
1633
+ const result = await postProjectServiceJsonOrLocal("/threads/open", {
1200
1634
  title: opts.title,
1635
+ from: opts.from,
1636
+ participants,
1201
1637
  kind: opts.kind ?? "conversation",
1202
- createdBy: opts.from,
1203
- participants: [...new Set([opts.from, ...participants])],
1204
- });
1205
- console.log(thread.id);
1638
+ }, () => ({
1639
+ thread: createThread({
1640
+ title: opts.title,
1641
+ kind: opts.kind ?? "conversation",
1642
+ createdBy: opts.from,
1643
+ participants: [...new Set([opts.from, ...participants])],
1644
+ }),
1645
+ }));
1646
+ console.log(result.thread.id);
1206
1647
  });
1207
1648
  threadCmd
1208
1649
  .command("send")
@@ -1212,36 +1653,46 @@ threadCmd
1212
1653
  .requiredOption("--from <sessionId>", "Sending session")
1213
1654
  .option("--to <ids>", "Comma-separated recipient session ids")
1214
1655
  .option("--kind <kind>", "request|reply|status|decision|handoff|note", "note")
1215
- .action((threadId, body, opts) => {
1216
- const thread = readThread(threadId);
1217
- if (!thread) {
1218
- console.error(`aimux: thread not found: ${threadId}`);
1219
- process.exit(1);
1220
- }
1656
+ .action(async (threadId, body, opts) => {
1221
1657
  const to = opts.to
1222
1658
  ?.split(",")
1223
1659
  .map((value) => value.trim())
1224
1660
  .filter(Boolean);
1225
- const message = sendThreadMessage({
1661
+ const result = await postProjectServiceJsonOrLocal("/threads/send", {
1226
1662
  threadId,
1227
1663
  from: opts.from,
1228
1664
  to,
1229
1665
  kind: opts.kind ?? "note",
1230
1666
  body,
1231
- }).message;
1232
- console.log(message.id);
1667
+ }, () => {
1668
+ if (!readThread(threadId)) {
1669
+ console.error(`aimux: thread not found: ${threadId}`);
1670
+ process.exit(1);
1671
+ }
1672
+ return sendThreadMessage({
1673
+ threadId,
1674
+ from: opts.from,
1675
+ to,
1676
+ kind: opts.kind ?? "note",
1677
+ body,
1678
+ });
1679
+ });
1680
+ console.log(result.message.id);
1233
1681
  });
1234
1682
  threadCmd
1235
1683
  .command("mark-seen")
1236
1684
  .description("Mark a thread as seen for a participant")
1237
1685
  .argument("<threadId>")
1238
1686
  .requiredOption("--session <sessionId>", "Participant session id")
1239
- .action((threadId, opts) => {
1240
- const thread = markThreadSeen(threadId, opts.session);
1241
- if (!thread) {
1242
- console.error(`aimux: thread not found: ${threadId}`);
1243
- process.exit(1);
1244
- }
1687
+ .action(async (threadId, opts) => {
1688
+ await postProjectServiceJsonOrLocal("/threads/mark-seen", { threadId, session: opts.session }, () => {
1689
+ const thread = markThreadSeen(threadId, opts.session);
1690
+ if (!thread) {
1691
+ console.error(`aimux: thread not found: ${threadId}`);
1692
+ process.exit(1);
1693
+ }
1694
+ return { ok: true, thread };
1695
+ });
1245
1696
  console.log("ok");
1246
1697
  });
1247
1698
  threadCmd
@@ -1381,7 +1832,11 @@ handoffCmd
1381
1832
  catch {
1382
1833
  const result = sendHandoff({
1383
1834
  from: opts.from ?? "user",
1384
- to: to ?? [],
1835
+ to: to?.length
1836
+ ? to
1837
+ : [opts.assignee, opts.tool]
1838
+ .map((value) => value?.trim())
1839
+ .filter((value) => Boolean(value)),
1385
1840
  body,
1386
1841
  title: opts.title,
1387
1842
  worktreePath: opts.worktree,
@@ -1451,10 +1906,20 @@ taskCmd
1451
1906
  .option("--session <sessionId>", "Filter to tasks assigned to or created by a session")
1452
1907
  .option("--status <status>", "Filter by task status")
1453
1908
  .option("--json", "Emit JSON")
1454
- .action((opts) => {
1455
- const tasks = readAllTasks()
1456
- .filter((task) => !opts.session || task.assignedTo === opts.session || task.assignedBy === opts.session)
1457
- .filter((task) => !opts.status || task.status === opts.status);
1909
+ .action(async (opts) => {
1910
+ const params = new URLSearchParams();
1911
+ if (opts.session)
1912
+ params.set("session", opts.session);
1913
+ if (opts.status)
1914
+ params.set("status", opts.status);
1915
+ const query = params.toString();
1916
+ const result = await getProjectServiceJsonOrLocal(`/tasks${query ? `?${query}` : ""}`, () => ({
1917
+ ok: true,
1918
+ tasks: readAllTasks()
1919
+ .filter((task) => !opts.session || task.assignedTo === opts.session || task.assignedBy === opts.session)
1920
+ .filter((task) => !opts.status || task.status === opts.status),
1921
+ }));
1922
+ const tasks = Array.isArray(result.tasks) ? result.tasks : [];
1458
1923
  if (opts.json) {
1459
1924
  console.log(JSON.stringify({ tasks }, null, 2));
1460
1925
  return;
@@ -1475,14 +1940,23 @@ taskCmd
1475
1940
  .description("Show an orchestrated task")
1476
1941
  .argument("<taskId>")
1477
1942
  .option("--json", "Emit JSON")
1478
- .action((taskId, opts) => {
1479
- const task = readTask(taskId);
1480
- if (!task) {
1943
+ .action(async (taskId, opts) => {
1944
+ const detail = await getProjectServiceJsonOrLocal(`/tasks/${encodeURIComponent(taskId)}`, () => {
1945
+ const task = readTask(taskId);
1946
+ if (!task)
1947
+ return null;
1948
+ return {
1949
+ ok: true,
1950
+ task,
1951
+ thread: task.threadId ? readThread(task.threadId) : undefined,
1952
+ messages: task.threadId ? readMessages(task.threadId) : [],
1953
+ };
1954
+ });
1955
+ if (!detail?.task) {
1481
1956
  console.error(`aimux: task not found: ${taskId}`);
1482
1957
  process.exit(1);
1483
1958
  }
1484
- const thread = task.threadId ? readThread(task.threadId) : undefined;
1485
- const messages = task.threadId ? readMessages(task.threadId) : [];
1959
+ const { task, thread, messages } = detail;
1486
1960
  if (opts.json) {
1487
1961
  console.log(JSON.stringify({ task, thread, messages }, null, 2));
1488
1962
  return;
@@ -1743,12 +2217,17 @@ worktreeCmd
1743
2217
  .option("--json", "Emit JSON")
1744
2218
  .action(async (opts) => {
1745
2219
  const projectRoot = await prepareProjectContext(opts.project);
1746
- const worktrees = listWorktrees(projectRoot);
2220
+ await ensureDaemonProjectReadyForFallback(projectRoot);
2221
+ const result = await getLiveProjectServiceJsonOrLocal(projectRoot, "/worktrees", () => ({
2222
+ ok: true,
2223
+ worktrees: listVisibleLocalWorktrees(projectRoot),
2224
+ }));
2225
+ const worktrees = result.worktrees ?? [];
1747
2226
  if (opts.json) {
1748
2227
  console.log(JSON.stringify(worktrees, null, 2));
1749
2228
  return;
1750
2229
  }
1751
- printWorktrees(projectRoot);
2230
+ printWorktrees(projectRoot, worktrees);
1752
2231
  });
1753
2232
  worktreeCmd
1754
2233
  .command("create <name>")
@@ -1758,7 +2237,12 @@ worktreeCmd
1758
2237
  .action(async (name, opts) => {
1759
2238
  try {
1760
2239
  const projectRoot = await prepareProjectContext(opts.project);
1761
- const createdPath = createWorktree(name, projectRoot);
2240
+ await ensureDaemonProjectReadyForFallback(projectRoot);
2241
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/worktrees/create", { name }, () => {
2242
+ const mux = new Multiplexer();
2243
+ return mux.createDesktopWorktree(name);
2244
+ });
2245
+ const createdPath = result.path;
1762
2246
  if (opts.json) {
1763
2247
  console.log(JSON.stringify({
1764
2248
  ok: true,
@@ -1776,6 +2260,114 @@ worktreeCmd
1776
2260
  process.exit(1);
1777
2261
  }
1778
2262
  });
2263
+ worktreeCmd
2264
+ .command("remove <path>")
2265
+ .description("Remove a git worktree")
2266
+ .option("--project <path>", "Project path")
2267
+ .option("--json", "Emit JSON")
2268
+ .action(async (targetPath, opts) => {
2269
+ try {
2270
+ const inputCwd = process.cwd();
2271
+ const resolvedPath = pathResolve(inputCwd, targetPath);
2272
+ const projectRoot = await prepareProjectContext(opts.project);
2273
+ await ensureDaemonProjectReadyForFallback(projectRoot);
2274
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/worktrees/remove", { path: resolvedPath }, () => {
2275
+ const mux = new Multiplexer();
2276
+ return mux.removeDesktopWorktree(resolvedPath);
2277
+ });
2278
+ if (opts.json) {
2279
+ console.log(JSON.stringify({ ok: true, projectRoot, path: result.path, status: result.status }, null, 2));
2280
+ return;
2281
+ }
2282
+ console.log(`${result.status === "removing" ? "removing" : "removed"} ${result.path}`);
2283
+ }
2284
+ catch (err) {
2285
+ const msg = err instanceof Error ? err.message : String(err);
2286
+ console.error(`Error: ${msg}`);
2287
+ process.exit(1);
2288
+ }
2289
+ });
2290
+ worktreeCmd
2291
+ .command("graveyard <path>")
2292
+ .description("Move a worktree to the graveyard without deleting the checkout")
2293
+ .option("--project <path>", "Project path")
2294
+ .option("--json", "Emit JSON")
2295
+ .action(async (targetPath, opts) => {
2296
+ try {
2297
+ const inputCwd = process.cwd();
2298
+ const resolvedPath = pathResolve(inputCwd, targetPath);
2299
+ const projectRoot = await prepareProjectContext(opts.project);
2300
+ await ensureDaemonProjectReadyForFallback(projectRoot);
2301
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/worktrees/graveyard", { path: resolvedPath }, () => {
2302
+ const mux = new Multiplexer();
2303
+ return mux.graveyardDesktopWorktree(resolvedPath);
2304
+ });
2305
+ if (opts.json) {
2306
+ console.log(JSON.stringify({ ok: true, projectRoot, path: result.path, status: result.status }, null, 2));
2307
+ return;
2308
+ }
2309
+ console.log(`graveyarded ${result.path}`);
2310
+ }
2311
+ catch (err) {
2312
+ const msg = err instanceof Error ? err.message : String(err);
2313
+ console.error(`Error: ${msg}`);
2314
+ process.exit(1);
2315
+ }
2316
+ });
2317
+ worktreeCmd
2318
+ .command("resurrect <path>")
2319
+ .description("Restore a graveyarded worktree to the active worktree list")
2320
+ .option("--project <path>", "Project path")
2321
+ .option("--json", "Emit JSON")
2322
+ .action(async (targetPath, opts) => {
2323
+ try {
2324
+ const inputCwd = process.cwd();
2325
+ const resolvedPath = pathResolve(inputCwd, targetPath);
2326
+ const projectRoot = await prepareProjectContext(opts.project);
2327
+ await ensureDaemonProjectReadyForFallback(projectRoot);
2328
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/graveyard/worktrees/resurrect", { path: resolvedPath }, () => {
2329
+ const mux = new Multiplexer();
2330
+ return mux.resurrectGraveyardWorktree(resolvedPath);
2331
+ });
2332
+ if (opts.json) {
2333
+ console.log(JSON.stringify({ ok: true, projectRoot, path: result.path, status: result.status }, null, 2));
2334
+ return;
2335
+ }
2336
+ console.log(`resurrected ${result.path}`);
2337
+ }
2338
+ catch (err) {
2339
+ const msg = err instanceof Error ? err.message : String(err);
2340
+ console.error(`Error: ${msg}`);
2341
+ process.exit(1);
2342
+ }
2343
+ });
2344
+ worktreeCmd
2345
+ .command("delete-graveyard <path>")
2346
+ .description("Permanently delete a graveyarded worktree entry")
2347
+ .option("--project <path>", "Project path")
2348
+ .option("--json", "Emit JSON")
2349
+ .action(async (targetPath, opts) => {
2350
+ try {
2351
+ const inputCwd = process.cwd();
2352
+ const resolvedPath = pathResolve(inputCwd, targetPath);
2353
+ const projectRoot = await prepareProjectContext(opts.project);
2354
+ await ensureDaemonProjectReadyForFallback(projectRoot);
2355
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/graveyard/worktrees/delete", { path: resolvedPath }, () => {
2356
+ const mux = new Multiplexer();
2357
+ return mux.deleteGraveyardWorktree(resolvedPath);
2358
+ });
2359
+ if (opts.json) {
2360
+ console.log(JSON.stringify({ ok: true, projectRoot, path: result.path, status: result.status }, null, 2));
2361
+ return;
2362
+ }
2363
+ console.log(`deleted ${result.path}`);
2364
+ }
2365
+ catch (err) {
2366
+ const msg = err instanceof Error ? err.message : String(err);
2367
+ console.error(`Error: ${msg}`);
2368
+ process.exit(1);
2369
+ }
2370
+ });
1779
2371
  program
1780
2372
  .command("spawn")
1781
2373
  .description("Spawn a fresh agent session using the same flow as the dashboard")
@@ -1868,27 +2460,26 @@ graveyardCmd
1868
2460
  .option("--project <path>", "Project path")
1869
2461
  .option("--json", "Emit JSON")
1870
2462
  .action(async (opts) => {
1871
- await prepareProjectContext(opts.project);
1872
- const graveyardPath = getGraveyardPath();
2463
+ const projectRoot = await prepareProjectContext(opts.project);
2464
+ await ensureDaemonProjectReadyForFallback(projectRoot);
1873
2465
  try {
1874
- const graveyard = JSON.parse(readFileSync(graveyardPath, "utf-8"));
2466
+ const graveyard = await getLiveProjectServiceJsonOrLocal(projectRoot, "/graveyard", () => ({
2467
+ ok: true,
2468
+ entries: listTopologySessionStates({ statuses: ["graveyard"] }),
2469
+ worktrees: listTopologyWorktreeGraveyard(),
2470
+ }));
1875
2471
  if (opts.json) {
1876
- console.log(JSON.stringify(Array.isArray(graveyard) ? graveyard : [], null, 2));
1877
- return;
1878
- }
1879
- if (!Array.isArray(graveyard) || graveyard.length === 0) {
1880
- console.log("Graveyard is empty.");
2472
+ console.log(JSON.stringify({
2473
+ entries: Array.isArray(graveyard.entries) ? graveyard.entries : [],
2474
+ worktrees: Array.isArray(graveyard.worktrees) ? graveyard.worktrees : [],
2475
+ }, null, 2));
1881
2476
  return;
1882
2477
  }
1883
- console.log("ID".padEnd(25) + "Tool".padEnd(15) + "Backend Session ID");
1884
- console.log("-".repeat(70));
1885
- for (const s of graveyard) {
1886
- console.log((s.id ?? "?").padEnd(25) + (s.command ?? s.tool ?? "?").padEnd(15) + (s.backendSessionId ?? "(none)"));
1887
- }
2478
+ printGraveyard(graveyard);
1888
2479
  }
1889
2480
  catch {
1890
2481
  if (opts.json) {
1891
- console.log("[]");
2482
+ console.log(JSON.stringify({ entries: [], worktrees: [] }, null, 2));
1892
2483
  return;
1893
2484
  }
1894
2485
  console.log("Graveyard is empty.");
@@ -1902,8 +2493,11 @@ graveyardCmd
1902
2493
  .action(async (id, opts) => {
1903
2494
  try {
1904
2495
  const projectRoot = await prepareProjectContext(opts.project);
1905
- const mux = new Multiplexer();
1906
- const result = await mux.sendAgentToGraveyard(id);
2496
+ await ensureDaemonProjectReady(projectRoot);
2497
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/agents/kill", { sessionId: id }, () => {
2498
+ const mux = new Multiplexer();
2499
+ return mux.sendAgentToGraveyard(id);
2500
+ });
1907
2501
  if (opts.json) {
1908
2502
  console.log(JSON.stringify({
1909
2503
  ok: true,
@@ -1928,44 +2522,23 @@ graveyardCmd
1928
2522
  .option("--project <path>", "Project path")
1929
2523
  .option("--json", "Emit JSON")
1930
2524
  .action(async (id, opts) => {
1931
- await prepareProjectContext(opts.project);
1932
- const graveyardPath = getGraveyardPath();
1933
- if (!existsSync(graveyardPath)) {
1934
- console.error("Graveyard is empty.");
1935
- process.exit(1);
1936
- }
1937
2525
  try {
1938
- const graveyard = JSON.parse(readFileSync(graveyardPath, "utf-8"));
1939
- const idx = graveyard.findIndex((s) => s.id === id);
1940
- if (idx === -1) {
1941
- console.error(`Agent "${id}" not found in graveyard.`);
1942
- process.exit(1);
1943
- }
1944
- const restored = graveyard.splice(idx, 1)[0];
1945
- writeFileSync(graveyardPath, JSON.stringify(graveyard, null, 2) + "\n");
1946
- const statePath = getStatePath();
1947
- let state = {
1948
- savedAt: new Date().toISOString(),
1949
- cwd: process.cwd(),
1950
- sessions: [],
1951
- };
1952
- if (existsSync(statePath)) {
1953
- try {
1954
- state = JSON.parse(readFileSync(statePath, "utf-8"));
1955
- }
1956
- catch { }
1957
- }
1958
- state.sessions.push(restored);
1959
- writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
2526
+ const projectRoot = await prepareProjectContext(opts.project);
2527
+ await ensureDaemonProjectReady(projectRoot);
2528
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/graveyard/resurrect", { sessionId: id }, () => {
2529
+ const mux = new Multiplexer();
2530
+ return mux.resurrectGraveyardSession(id);
2531
+ });
1960
2532
  if (opts.json) {
1961
2533
  console.log(JSON.stringify({
1962
2534
  ok: true,
1963
- sessionId: id,
1964
- status: "offline",
2535
+ projectRoot,
2536
+ sessionId: result.sessionId,
2537
+ status: result.status,
1965
2538
  }, null, 2));
1966
2539
  return;
1967
2540
  }
1968
- console.log(`Resurrected "${id}". It will appear as offline next time you start aimux.`);
2541
+ console.log(`resurrected ${result.sessionId}`);
1969
2542
  }
1970
2543
  catch (err) {
1971
2544
  const msg = err instanceof Error ? err.message : String(err);
@@ -2060,8 +2633,11 @@ program
2060
2633
  .action(async (sessionId, opts) => {
2061
2634
  try {
2062
2635
  const projectRoot = await prepareProjectContext(opts.project);
2063
- const mux = new Multiplexer();
2064
- const result = await mux.sendAgentToGraveyard(sessionId);
2636
+ await ensureDaemonProjectReady(projectRoot);
2637
+ const result = await postLiveProjectServiceJsonOrLocal(projectRoot, "/agents/kill", { sessionId }, () => {
2638
+ const mux = new Multiplexer();
2639
+ return mux.sendAgentToGraveyard(sessionId);
2640
+ });
2065
2641
  if (opts.json) {
2066
2642
  console.log(JSON.stringify({
2067
2643
  ok: true,
@@ -2113,6 +2689,75 @@ program
2113
2689
  const statuslineCmd = program.command("statusline").description("Manage Claude Code statusline integration");
2114
2690
  const doctorCmd = program.command("doctor").description("Inspect aimux runtime state");
2115
2691
  const repairCmd = program.command("repair").description("Repair the current project runtime in place");
2692
+ program
2693
+ .command("debug-state <target>")
2694
+ .description("Read-only debug snapshot for one session, service, backend session, or worktree")
2695
+ .action((target) => {
2696
+ const report = buildDebugStateReport({ cwd: process.cwd(), target });
2697
+ console.log(renderDebugStateReport(report));
2698
+ });
2699
+ const migrationCmd = program
2700
+ .command("migration")
2701
+ .description("Explicit runtime-core migration audit, import, and rollback tooling");
2702
+ migrationCmd
2703
+ .command("audit")
2704
+ .description("Inspect legacy runtime artifacts without mutating project state")
2705
+ .option("--project <path>", "Project path", process.cwd())
2706
+ .action((opts) => {
2707
+ const projectRoot = resolveProjectRoot(pathResolve(opts.project));
2708
+ console.log(renderRuntimeMigrationReport(buildRuntimeMigrationReport({ cwd: projectRoot })));
2709
+ });
2710
+ migrationCmd
2711
+ .command("import")
2712
+ .description("Import legacy exchange artifacts into runtime-exchange.yaml with a rollback manifest")
2713
+ .option("--project <path>", "Project path", process.cwd())
2714
+ .action(async (opts) => {
2715
+ const projectRoot = resolveProjectRoot(pathResolve(opts.project));
2716
+ await initPaths(projectRoot);
2717
+ console.log(renderRuntimeMigrationImportResult(importRuntimeMigration({ cwd: projectRoot })));
2718
+ });
2719
+ migrationCmd
2720
+ .command("rollback <manifest>")
2721
+ .description("Restore files recorded by a runtime migration manifest")
2722
+ .action((manifest) => {
2723
+ console.log(renderRuntimeMigrationRollbackResult(rollbackRuntimeMigration(pathResolve(manifest))));
2724
+ });
2725
+ const logsCmd = program.command("logs").description("Inspect persistent aimux logs");
2726
+ logsCmd
2727
+ .command("path")
2728
+ .description("Print the active log file path")
2729
+ .option("--daemon", "Show the global daemon log path")
2730
+ .option("--project <path>", "Project path")
2731
+ .action((opts) => {
2732
+ console.log(selectedLogPath(opts));
2733
+ });
2734
+ logsCmd
2735
+ .command("tail")
2736
+ .description("Print recent log lines")
2737
+ .option("--daemon", "Tail the global daemon log")
2738
+ .option("--project <path>", "Project path")
2739
+ .option("-n, --lines <number>", "Number of lines to print", "80")
2740
+ .action((opts) => {
2741
+ const path = selectedLogPath(opts);
2742
+ const output = readLastLogLines(path, parseLineCount(opts.lines));
2743
+ if (output) {
2744
+ console.log(output);
2745
+ return;
2746
+ }
2747
+ console.error(`No log entries at ${path}`);
2748
+ process.exit(1);
2749
+ });
2750
+ logsCmd
2751
+ .command("clear")
2752
+ .description("Clear the active log file")
2753
+ .option("--daemon", "Clear the global daemon log")
2754
+ .option("--project <path>", "Project path")
2755
+ .action((opts) => {
2756
+ const path = selectedLogPath(opts);
2757
+ mkdirSync(pathDirname(path), { recursive: true });
2758
+ writeFileSync(path, "");
2759
+ console.log(`Cleared ${path}`);
2760
+ });
2116
2761
  doctorCmd
2117
2762
  .command("tmux")
2118
2763
  .description("Inspect managed tmux runtime state")
@@ -2232,20 +2877,29 @@ program
2232
2877
  force: true,
2233
2878
  }, () => {
2234
2879
  const kind = (opts.kind?.trim() || "notification");
2235
- const notification = upsertNotification({
2880
+ const sessionId = opts.session?.trim() || undefined;
2881
+ const context = sessionId ? metadataDisplayContext(loadMetadataState().sessions[sessionId]) : undefined;
2882
+ const alert = contextualizeAlertInput({
2883
+ kind,
2884
+ sessionId,
2236
2885
  title,
2886
+ message: [opts.subtitle?.trim(), body].filter(Boolean).join(" — "),
2887
+ forceNotify: true,
2888
+ }, context);
2889
+ const notification = upsertNotification({
2890
+ title: alert.title,
2237
2891
  subtitle: opts.subtitle?.trim() || undefined,
2238
2892
  body,
2239
- sessionId: opts.session?.trim() || undefined,
2893
+ sessionId,
2240
2894
  kind,
2241
2895
  });
2242
2896
  notifyAlert({
2243
2897
  type: "alert",
2244
2898
  kind,
2245
2899
  projectId: getProjectId(),
2246
- sessionId: opts.session?.trim() || undefined,
2247
- title,
2248
- message: [opts.subtitle?.trim(), body].filter(Boolean).join(" — "),
2900
+ sessionId,
2901
+ title: alert.title,
2902
+ message: alert.message,
2249
2903
  ts: notification.createdAt,
2250
2904
  forceNotify: true,
2251
2905
  });
@@ -2271,6 +2925,9 @@ program
2271
2925
  const payload = parseClaudeHookPayload(rawInput);
2272
2926
  const sessionId = await resolveClaudeHookSessionId(opts.session, payload.session_id);
2273
2927
  const result = { ok: true, action, sessionId };
2928
+ if (payload.session_id) {
2929
+ result.backendSessionId = payload.session_id;
2930
+ }
2274
2931
  const setActivity = async (activity) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-activity", { session: sessionId, activity }, () => metadataTracker.setActivity(sessionId, activity, projectRoot));
2275
2932
  const setAttention = async (attention) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-attention", { session: sessionId, attention }, () => metadataTracker.setAttention(sessionId, attention, projectRoot));
2276
2933
  const emitEvent = async (kind, message, tone) => postLiveProjectServiceJsonOrLocal(projectRoot, "/event", { session: sessionId, event: { kind, message, tone } }, () => metadataTracker.emit(sessionId, { kind, message, tone }, projectRoot));
@@ -2278,6 +2935,21 @@ program
2278
2935
  ok: true,
2279
2936
  cleared: clearNotifications({ sessionId }),
2280
2937
  }));
2938
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path.trim() : "";
2939
+ if (transcriptPath) {
2940
+ const context = { transcriptPath };
2941
+ await postLiveProjectServiceJsonOrLocal(projectRoot, "/set-context", { session: sessionId, context }, () => {
2942
+ updateSessionMetadata(sessionId, (current) => ({
2943
+ ...current,
2944
+ context: {
2945
+ ...(current.context ?? {}),
2946
+ ...context,
2947
+ },
2948
+ }), projectRoot);
2949
+ return { ok: true };
2950
+ });
2951
+ result.transcriptPath = transcriptPath;
2952
+ }
2281
2953
  switch (action) {
2282
2954
  case "session-start":
2283
2955
  case "active":