aimux-cli 0.1.16 → 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 +821 -151
  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 +28 -12
  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,6 +440,69 @@ 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
+ }
374
506
  const pkgJsonPath = pathJoin(pathDirname(fileURLToPath(import.meta.url)), "..", "package.json");
375
507
  const pkgVersion = JSON.parse(readFileSync(pkgJsonPath, "utf8")).version;
376
508
  program
@@ -382,11 +514,27 @@ program
382
514
  .option("--resume", "Resume previous sessions using native tool resume")
383
515
  .option("--restore", "Start fresh sessions with injected history context")
384
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")
385
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
+ }
386
527
  const opts = typeof actionCommand?.opts === "function" ? actionCommand.opts() : {};
387
- 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;
388
535
  const projectRoot = requestedProject ? resolveProjectRoot(pathResolve(requestedProject)) : undefined;
389
536
  await initPaths(projectRoot);
537
+ configureLoggingForCommand(actionCommand);
390
538
  })
391
539
  .action(async (tool, args, opts) => {
392
540
  const originalCwd = process.cwd();
@@ -447,11 +595,19 @@ program
447
595
  process.on("SIGTERM", shutdown);
448
596
  process.on("uncaughtException", (err) => {
449
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
+ });
450
602
  console.error(err);
451
603
  process.exit(1);
452
604
  });
453
605
  process.on("unhandledRejection", (reason) => {
454
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
+ });
455
611
  console.error(reason);
456
612
  process.exit(1);
457
613
  });
@@ -533,8 +689,11 @@ program
533
689
  try {
534
690
  if (sessionId) {
535
691
  const projectRoot = await prepareProjectContext(opts.project);
536
- const mux = new Multiplexer();
537
- 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
+ });
538
697
  if (opts.json) {
539
698
  console.log(JSON.stringify({
540
699
  ok: true,
@@ -557,6 +716,7 @@ program
557
716
  projectServiceStopped: result.projectServiceStopped,
558
717
  tmuxSessionsKilled: result.tmuxSessionsKilled,
559
718
  }, null, 2));
719
+ process.exitCode = 1;
560
720
  return;
561
721
  }
562
722
  console.log(`Stopped project runtime for ${projectRoot}`);
@@ -608,6 +768,47 @@ program
608
768
  const hostCmd = program
609
769
  .command("host")
610
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
+ });
611
812
  program
612
813
  .command("serve")
613
814
  .description("Advanced: ensure the legacy daemon-backed project control service is running")
@@ -720,27 +921,22 @@ hostCmd
720
921
  console.log(`Restarted project service for ${dashboardSession.sessionName}`);
721
922
  });
722
923
  hostCmd
723
- .command("agent-send")
724
- .description("Send raw input to a running agent session over the project HTTP service")
725
- .argument("<sessionId>", "Agent session ID")
726
- .argument("[data...]", "Input to send")
727
- .option("--stdin", "Read the full input payload from stdin")
728
- .option("--submit", "Submit after writing the input")
729
- .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) => {
730
929
  await initPaths();
731
- const payload = opts.stdin === true ? await readAllStdin() : data.join(" ");
732
- if (!payload) {
733
- throw new Error("input data is required");
734
- }
735
- const result = await postProjectServiceJson("/agents/input", {
736
- sessionId,
737
- data: payload,
738
- submit: opts.submit === true,
739
- });
740
- if (result.accepted === false) {
741
- 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;
742
934
  }
743
- 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);
744
940
  });
745
941
  hostCmd
746
942
  .command("agent-read")
@@ -930,9 +1126,20 @@ daemonCmd
930
1126
  .action(async (opts) => {
931
1127
  const info = loadDaemonInfo();
932
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
+ }
933
1139
  const payload = {
934
1140
  daemon: info,
935
1141
  projects: Object.values(state.projects),
1142
+ relay,
936
1143
  };
937
1144
  if (opts.json) {
938
1145
  console.log(JSON.stringify(payload, null, 2));
@@ -944,6 +1151,13 @@ daemonCmd
944
1151
  }
945
1152
  console.log(`Daemon pid=${info.pid} port=${info.port}`);
946
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
+ }
947
1161
  });
948
1162
  daemonCmd
949
1163
  .command("projects")
@@ -1003,11 +1217,19 @@ program
1003
1217
  process.on("SIGTERM", shutdown);
1004
1218
  process.on("uncaughtException", (err) => {
1005
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
+ });
1006
1224
  console.error(err);
1007
1225
  process.exit(1);
1008
1226
  });
1009
1227
  process.on("unhandledRejection", (reason) => {
1010
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
+ });
1011
1233
  console.error(reason);
1012
1234
  process.exit(1);
1013
1235
  });
@@ -1041,15 +1263,8 @@ projectsCmd
1041
1263
  return;
1042
1264
  }
1043
1265
  for (const project of projects) {
1044
- const liveBadge = project.sessions.some((session) => session.status !== "offline") ? "live" : "idle";
1266
+ const liveBadge = project.serviceAlive ? "live" : "idle";
1045
1267
  console.log(`${project.name} ${liveBadge} ${project.path}`);
1046
- if (project.sessions.length === 0)
1047
- continue;
1048
- for (const session of project.sessions) {
1049
- const label = session.label ? ` ${session.label}` : "";
1050
- const headline = session.headline ? ` - ${session.headline}` : "";
1051
- console.log(` ${session.id} ${session.tool} ${session.status}${label}${headline}`);
1052
- }
1053
1268
  }
1054
1269
  });
1055
1270
  program
@@ -1075,6 +1290,166 @@ program
1075
1290
  llmCompact(sessionIds);
1076
1291
  console.log(`Done. Summary written to ${getContextDir()}/summary.md`);
1077
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
+ });
1078
1453
  async function prepareProjectContext(requestedProject) {
1079
1454
  const requestedPath = pathResolve(requestedProject ?? process.cwd());
1080
1455
  const projectRoot = resolveProjectRoot(requestedPath);
@@ -1082,9 +1457,13 @@ async function prepareProjectContext(requestedProject) {
1082
1457
  process.chdir(projectRoot);
1083
1458
  return projectRoot;
1084
1459
  }
1085
- 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) {
1086
1465
  try {
1087
- const worktrees = listWorktrees(projectRoot);
1466
+ const worktrees = worktreesInput ?? listWorktrees(projectRoot);
1088
1467
  if (worktrees.length === 0) {
1089
1468
  console.log("No worktrees found.");
1090
1469
  return;
@@ -1101,9 +1480,55 @@ function printWorktrees(projectRoot) {
1101
1480
  process.exit(1);
1102
1481
  }
1103
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
+ }
1104
1513
  const worktreeCmd = program.command("worktree").description("Manage git worktrees");
1105
- worktreeCmd.action(() => {
1106
- 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 ?? []);
1107
1532
  });
1108
1533
  const threadCmd = program.command("thread").description("Inspect and manage orchestration threads");
1109
1534
  program
@@ -1111,8 +1536,9 @@ program
1111
1536
  .description("Alias for thread list")
1112
1537
  .option("--session <sessionId>", "Filter to threads involving a session")
1113
1538
  .option("--json", "Emit JSON")
1114
- .action((opts) => {
1115
- 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));
1116
1542
  if (opts.json) {
1117
1543
  console.log(JSON.stringify(summaries, null, 2));
1118
1544
  return;
@@ -1136,8 +1562,9 @@ threadCmd
1136
1562
  .description("List orchestration threads")
1137
1563
  .option("--session <sessionId>", "Filter to threads involving a session")
1138
1564
  .option("--json", "Emit JSON")
1139
- .action((opts) => {
1140
- 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));
1141
1568
  if (opts.json) {
1142
1569
  console.log(JSON.stringify(summaries, null, 2));
1143
1570
  return;
@@ -1161,13 +1588,18 @@ threadCmd
1161
1588
  .description("Show a thread and its messages")
1162
1589
  .argument("<threadId>")
1163
1590
  .option("--json", "Emit JSON")
1164
- .action((threadId, opts) => {
1165
- const thread = readThread(threadId);
1166
- 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) {
1167
1599
  console.error(`aimux: thread not found: ${threadId}`);
1168
1600
  process.exit(1);
1169
1601
  }
1170
- const messages = readMessages(threadId);
1602
+ const { thread, messages } = detail;
1171
1603
  if (opts.json) {
1172
1604
  console.log(JSON.stringify({ thread, messages }, null, 2));
1173
1605
  return;
@@ -1193,18 +1625,25 @@ threadCmd
1193
1625
  .requiredOption("--from <sessionId>", "Creating session")
1194
1626
  .requiredOption("--participants <ids>", "Comma-separated participant session ids")
1195
1627
  .option("--kind <kind>", "conversation|task|review|handoff|user", "conversation")
1196
- .action((opts) => {
1628
+ .action(async (opts) => {
1197
1629
  const participants = opts.participants
1198
1630
  .split(",")
1199
1631
  .map((value) => value.trim())
1200
1632
  .filter(Boolean);
1201
- const thread = createThread({
1633
+ const result = await postProjectServiceJsonOrLocal("/threads/open", {
1202
1634
  title: opts.title,
1635
+ from: opts.from,
1636
+ participants,
1203
1637
  kind: opts.kind ?? "conversation",
1204
- createdBy: opts.from,
1205
- participants: [...new Set([opts.from, ...participants])],
1206
- });
1207
- 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);
1208
1647
  });
1209
1648
  threadCmd
1210
1649
  .command("send")
@@ -1214,36 +1653,46 @@ threadCmd
1214
1653
  .requiredOption("--from <sessionId>", "Sending session")
1215
1654
  .option("--to <ids>", "Comma-separated recipient session ids")
1216
1655
  .option("--kind <kind>", "request|reply|status|decision|handoff|note", "note")
1217
- .action((threadId, body, opts) => {
1218
- const thread = readThread(threadId);
1219
- if (!thread) {
1220
- console.error(`aimux: thread not found: ${threadId}`);
1221
- process.exit(1);
1222
- }
1656
+ .action(async (threadId, body, opts) => {
1223
1657
  const to = opts.to
1224
1658
  ?.split(",")
1225
1659
  .map((value) => value.trim())
1226
1660
  .filter(Boolean);
1227
- const message = sendThreadMessage({
1661
+ const result = await postProjectServiceJsonOrLocal("/threads/send", {
1228
1662
  threadId,
1229
1663
  from: opts.from,
1230
1664
  to,
1231
1665
  kind: opts.kind ?? "note",
1232
1666
  body,
1233
- }).message;
1234
- 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);
1235
1681
  });
1236
1682
  threadCmd
1237
1683
  .command("mark-seen")
1238
1684
  .description("Mark a thread as seen for a participant")
1239
1685
  .argument("<threadId>")
1240
1686
  .requiredOption("--session <sessionId>", "Participant session id")
1241
- .action((threadId, opts) => {
1242
- const thread = markThreadSeen(threadId, opts.session);
1243
- if (!thread) {
1244
- console.error(`aimux: thread not found: ${threadId}`);
1245
- process.exit(1);
1246
- }
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
+ });
1247
1696
  console.log("ok");
1248
1697
  });
1249
1698
  threadCmd
@@ -1383,7 +1832,11 @@ handoffCmd
1383
1832
  catch {
1384
1833
  const result = sendHandoff({
1385
1834
  from: opts.from ?? "user",
1386
- to: to ?? [],
1835
+ to: to?.length
1836
+ ? to
1837
+ : [opts.assignee, opts.tool]
1838
+ .map((value) => value?.trim())
1839
+ .filter((value) => Boolean(value)),
1387
1840
  body,
1388
1841
  title: opts.title,
1389
1842
  worktreePath: opts.worktree,
@@ -1453,10 +1906,20 @@ taskCmd
1453
1906
  .option("--session <sessionId>", "Filter to tasks assigned to or created by a session")
1454
1907
  .option("--status <status>", "Filter by task status")
1455
1908
  .option("--json", "Emit JSON")
1456
- .action((opts) => {
1457
- const tasks = readAllTasks()
1458
- .filter((task) => !opts.session || task.assignedTo === opts.session || task.assignedBy === opts.session)
1459
- .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 : [];
1460
1923
  if (opts.json) {
1461
1924
  console.log(JSON.stringify({ tasks }, null, 2));
1462
1925
  return;
@@ -1477,14 +1940,23 @@ taskCmd
1477
1940
  .description("Show an orchestrated task")
1478
1941
  .argument("<taskId>")
1479
1942
  .option("--json", "Emit JSON")
1480
- .action((taskId, opts) => {
1481
- const task = readTask(taskId);
1482
- 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) {
1483
1956
  console.error(`aimux: task not found: ${taskId}`);
1484
1957
  process.exit(1);
1485
1958
  }
1486
- const thread = task.threadId ? readThread(task.threadId) : undefined;
1487
- const messages = task.threadId ? readMessages(task.threadId) : [];
1959
+ const { task, thread, messages } = detail;
1488
1960
  if (opts.json) {
1489
1961
  console.log(JSON.stringify({ task, thread, messages }, null, 2));
1490
1962
  return;
@@ -1745,12 +2217,17 @@ worktreeCmd
1745
2217
  .option("--json", "Emit JSON")
1746
2218
  .action(async (opts) => {
1747
2219
  const projectRoot = await prepareProjectContext(opts.project);
1748
- 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 ?? [];
1749
2226
  if (opts.json) {
1750
2227
  console.log(JSON.stringify(worktrees, null, 2));
1751
2228
  return;
1752
2229
  }
1753
- printWorktrees(projectRoot);
2230
+ printWorktrees(projectRoot, worktrees);
1754
2231
  });
1755
2232
  worktreeCmd
1756
2233
  .command("create <name>")
@@ -1760,7 +2237,12 @@ worktreeCmd
1760
2237
  .action(async (name, opts) => {
1761
2238
  try {
1762
2239
  const projectRoot = await prepareProjectContext(opts.project);
1763
- 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;
1764
2246
  if (opts.json) {
1765
2247
  console.log(JSON.stringify({
1766
2248
  ok: true,
@@ -1778,6 +2260,114 @@ worktreeCmd
1778
2260
  process.exit(1);
1779
2261
  }
1780
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
+ });
1781
2371
  program
1782
2372
  .command("spawn")
1783
2373
  .description("Spawn a fresh agent session using the same flow as the dashboard")
@@ -1870,27 +2460,26 @@ graveyardCmd
1870
2460
  .option("--project <path>", "Project path")
1871
2461
  .option("--json", "Emit JSON")
1872
2462
  .action(async (opts) => {
1873
- await prepareProjectContext(opts.project);
1874
- const graveyardPath = getGraveyardPath();
2463
+ const projectRoot = await prepareProjectContext(opts.project);
2464
+ await ensureDaemonProjectReadyForFallback(projectRoot);
1875
2465
  try {
1876
- 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
+ }));
1877
2471
  if (opts.json) {
1878
- console.log(JSON.stringify(Array.isArray(graveyard) ? graveyard : [], null, 2));
1879
- return;
1880
- }
1881
- if (!Array.isArray(graveyard) || graveyard.length === 0) {
1882
- 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));
1883
2476
  return;
1884
2477
  }
1885
- console.log("ID".padEnd(25) + "Tool".padEnd(15) + "Backend Session ID");
1886
- console.log("-".repeat(70));
1887
- for (const s of graveyard) {
1888
- console.log((s.id ?? "?").padEnd(25) + (s.command ?? s.tool ?? "?").padEnd(15) + (s.backendSessionId ?? "(none)"));
1889
- }
2478
+ printGraveyard(graveyard);
1890
2479
  }
1891
2480
  catch {
1892
2481
  if (opts.json) {
1893
- console.log("[]");
2482
+ console.log(JSON.stringify({ entries: [], worktrees: [] }, null, 2));
1894
2483
  return;
1895
2484
  }
1896
2485
  console.log("Graveyard is empty.");
@@ -1904,8 +2493,11 @@ graveyardCmd
1904
2493
  .action(async (id, opts) => {
1905
2494
  try {
1906
2495
  const projectRoot = await prepareProjectContext(opts.project);
1907
- const mux = new Multiplexer();
1908
- 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
+ });
1909
2501
  if (opts.json) {
1910
2502
  console.log(JSON.stringify({
1911
2503
  ok: true,
@@ -1930,44 +2522,23 @@ graveyardCmd
1930
2522
  .option("--project <path>", "Project path")
1931
2523
  .option("--json", "Emit JSON")
1932
2524
  .action(async (id, opts) => {
1933
- await prepareProjectContext(opts.project);
1934
- const graveyardPath = getGraveyardPath();
1935
- if (!existsSync(graveyardPath)) {
1936
- console.error("Graveyard is empty.");
1937
- process.exit(1);
1938
- }
1939
2525
  try {
1940
- const graveyard = JSON.parse(readFileSync(graveyardPath, "utf-8"));
1941
- const idx = graveyard.findIndex((s) => s.id === id);
1942
- if (idx === -1) {
1943
- console.error(`Agent "${id}" not found in graveyard.`);
1944
- process.exit(1);
1945
- }
1946
- const restored = graveyard.splice(idx, 1)[0];
1947
- writeFileSync(graveyardPath, JSON.stringify(graveyard, null, 2) + "\n");
1948
- const statePath = getStatePath();
1949
- let state = {
1950
- savedAt: new Date().toISOString(),
1951
- cwd: process.cwd(),
1952
- sessions: [],
1953
- };
1954
- if (existsSync(statePath)) {
1955
- try {
1956
- state = JSON.parse(readFileSync(statePath, "utf-8"));
1957
- }
1958
- catch { }
1959
- }
1960
- state.sessions.push(restored);
1961
- 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
+ });
1962
2532
  if (opts.json) {
1963
2533
  console.log(JSON.stringify({
1964
2534
  ok: true,
1965
- sessionId: id,
1966
- status: "offline",
2535
+ projectRoot,
2536
+ sessionId: result.sessionId,
2537
+ status: result.status,
1967
2538
  }, null, 2));
1968
2539
  return;
1969
2540
  }
1970
- console.log(`Resurrected "${id}". It will appear as offline next time you start aimux.`);
2541
+ console.log(`resurrected ${result.sessionId}`);
1971
2542
  }
1972
2543
  catch (err) {
1973
2544
  const msg = err instanceof Error ? err.message : String(err);
@@ -2062,8 +2633,11 @@ program
2062
2633
  .action(async (sessionId, opts) => {
2063
2634
  try {
2064
2635
  const projectRoot = await prepareProjectContext(opts.project);
2065
- const mux = new Multiplexer();
2066
- 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
+ });
2067
2641
  if (opts.json) {
2068
2642
  console.log(JSON.stringify({
2069
2643
  ok: true,
@@ -2115,6 +2689,75 @@ program
2115
2689
  const statuslineCmd = program.command("statusline").description("Manage Claude Code statusline integration");
2116
2690
  const doctorCmd = program.command("doctor").description("Inspect aimux runtime state");
2117
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
+ });
2118
2761
  doctorCmd
2119
2762
  .command("tmux")
2120
2763
  .description("Inspect managed tmux runtime state")
@@ -2234,20 +2877,29 @@ program
2234
2877
  force: true,
2235
2878
  }, () => {
2236
2879
  const kind = (opts.kind?.trim() || "notification");
2237
- 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,
2238
2885
  title,
2886
+ message: [opts.subtitle?.trim(), body].filter(Boolean).join(" — "),
2887
+ forceNotify: true,
2888
+ }, context);
2889
+ const notification = upsertNotification({
2890
+ title: alert.title,
2239
2891
  subtitle: opts.subtitle?.trim() || undefined,
2240
2892
  body,
2241
- sessionId: opts.session?.trim() || undefined,
2893
+ sessionId,
2242
2894
  kind,
2243
2895
  });
2244
2896
  notifyAlert({
2245
2897
  type: "alert",
2246
2898
  kind,
2247
2899
  projectId: getProjectId(),
2248
- sessionId: opts.session?.trim() || undefined,
2249
- title,
2250
- message: [opts.subtitle?.trim(), body].filter(Boolean).join(" — "),
2900
+ sessionId,
2901
+ title: alert.title,
2902
+ message: alert.message,
2251
2903
  ts: notification.createdAt,
2252
2904
  forceNotify: true,
2253
2905
  });
@@ -2273,6 +2925,9 @@ program
2273
2925
  const payload = parseClaudeHookPayload(rawInput);
2274
2926
  const sessionId = await resolveClaudeHookSessionId(opts.session, payload.session_id);
2275
2927
  const result = { ok: true, action, sessionId };
2928
+ if (payload.session_id) {
2929
+ result.backendSessionId = payload.session_id;
2930
+ }
2276
2931
  const setActivity = async (activity) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-activity", { session: sessionId, activity }, () => metadataTracker.setActivity(sessionId, activity, projectRoot));
2277
2932
  const setAttention = async (attention) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-attention", { session: sessionId, attention }, () => metadataTracker.setAttention(sessionId, attention, projectRoot));
2278
2933
  const emitEvent = async (kind, message, tone) => postLiveProjectServiceJsonOrLocal(projectRoot, "/event", { session: sessionId, event: { kind, message, tone } }, () => metadataTracker.emit(sessionId, { kind, message, tone }, projectRoot));
@@ -2280,6 +2935,21 @@ program
2280
2935
  ok: true,
2281
2936
  cleared: clearNotifications({ sessionId }),
2282
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
+ }
2283
2953
  switch (action) {
2284
2954
  case "session-start":
2285
2955
  case "active":