quoxcli 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,1062 @@
1
+ /**
2
+ * `quox dev supervisor`, DEVSUITE-DEVMODE (quox#453 phase 15 follow-on).
3
+ *
4
+ * The headless runner that lets a dev host's resident agent answer its slot
5
+ * channel with no interactive session open. All of the actual picking,
6
+ * rate-limiting, prompt-building, turn-execution and state logic lives in
7
+ * `../core/supervisorEngine.js` and is unit tested there; this file is the
8
+ * thin wiring: resolve defaults from the resident bootstrap profile, build
9
+ * the injected fetch/post/spawn closures against the CLI's existing
10
+ * endpoint-resolution and client patterns, and drive the poll loop.
11
+ *
12
+ * Delivery: this needs no new delivery channel because the CLI is already
13
+ * on every dev-enabled host (/opt/quoxcli + /usr/local/bin/quox), and it
14
+ * already holds the resident's auth (see lib/core/residentAuth.js).
15
+ *
16
+ * A SEPARATE stream owns the server-side switch (writes to
17
+ * /api/v1/dev-servers/:hostId/supervisor). This module is read-only against
18
+ * it: it never writes to that endpoint.
19
+ */
20
+
21
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, unlinkSync } from 'node:fs';
22
+ import { homedir, userInfo } from 'node:os';
23
+ import { dirname, join } from 'node:path';
24
+ import { spawn, execFile } from 'node:child_process';
25
+ import { promisify } from 'node:util';
26
+
27
+ import { formatDetail, formatOutput, printSuccess, printWarning, printInfo } from '../core/output.js';
28
+ import { resolveEndpoint } from '../api/endpoints.js';
29
+ import { resolveOrgId } from './helpers.js';
30
+ import { loadResidentProfile, expandHome, deriveSupervisorEnv } from '../core/residentAuth.js';
31
+ import { createInitialPumpState, runPumpOnce } from '../core/residentPump.js';
32
+ import { createInitialAlarmState, runWardenPass, sendAlarm } from '../core/residentAlarm.js';
33
+ import {
34
+ DEFAULT_SETTINGS,
35
+ DEFAULT_IDLE_RECHECK_SECONDS,
36
+ DEFAULT_WAIT_FAILURE_THRESHOLD,
37
+ DEFAULT_MAX_MESSAGE_AGE_MINUTES,
38
+ DEFAULT_HEARTBEAT_INTERVAL_SECONDS,
39
+ resolveSettings,
40
+ buildSelfIdentity,
41
+ runPollOnce,
42
+ runClaudeTurn,
43
+ createInitialState,
44
+ attemptSlotClaim,
45
+ createClaimState,
46
+ shouldPostErrorNotice,
47
+ startIndependentHeartbeat,
48
+ } from '../core/supervisorEngine.js';
49
+ import {
50
+ SERVICE_NAME,
51
+ DEFAULT_RESTART_SEC,
52
+ buildUnitFile,
53
+ defaultUnitDir,
54
+ defaultUnitPath,
55
+ installSupervisorService,
56
+ uninstallSupervisorService,
57
+ getServiceInstallStatus,
58
+ realExecFn,
59
+ } from '../core/systemdInstaller.js';
60
+
61
+ export function defaultLogPath() {
62
+ return join(homedir(), '.quox', 'supervisor.log');
63
+ }
64
+
65
+ export function defaultStatePath() {
66
+ return join(homedir(), '.quox', 'supervisor-state.json');
67
+ }
68
+
69
+ // QUOXPERTICUS-REVISIT P4/P5: separate continuity files for the warden's
70
+ // pump (steer) and alarm (watchdog) state, so they persist across process
71
+ // restarts the same way supervisor-state.json does, without mixing shapes
72
+ // with the headless-answering state above.
73
+ export function defaultPumpStatePath() {
74
+ return join(homedir(), '.quox', 'warden-pump-state.json');
75
+ }
76
+
77
+ export function defaultAlarmStatePath() {
78
+ return join(homedir(), '.quox', 'warden-alarm-state.json');
79
+ }
80
+
81
+ // #619: which `quox` binary to bake into the Restart=always unit's ExecStart.
82
+ // Baking process.argv[1] (the CLI doing the install) is the trap: if that CLI
83
+ // lives in a MUTABLE tree — the shared quox-dashboard checkout that
84
+ // sessions branch-switch, or a per-session …-wt-<name> worktree — then a
85
+ // later checkout/reset silently changes or removes the deployed supervisor
86
+ // code under the running unit. A resident crash-looped to StartLimit and sat
87
+ // dark ~15 days exactly this way ("long-lived units must NOT exec from a live
88
+ // working tree"). Prefer the STABLE detached CLI worktree the rest of the box
89
+ // already treats as canonical ($QUOX_CLI_DIR or ~/quox-dashboard-cli — the
90
+ // same one quox-cli-refresh/#607 maintains). If we can only fall back to a
91
+ // mutable path, return mutable:true so the caller warns loudly.
92
+ export function resolveSupervisorBin({ quoxBin, argv1, home, existsFn = existsSync, env = process.env } = {}) {
93
+ if (quoxBin) return { bin: quoxBin, source: 'explicit (--quox-bin)', mutable: false };
94
+ const stableDir = env.QUOX_CLI_DIR || join(home || homedir(), 'quox-dashboard-cli');
95
+ const stableBin = join(stableDir, 'cli', 'bin', 'quox.js');
96
+ if (existsFn(stableBin)) return { bin: stableBin, source: 'stable CLI worktree', mutable: false };
97
+ const fallback = argv1 || 'quox';
98
+ // A per-session worktree (…-wt-…) or the shared mutable dev tree
99
+ // (…/quox-dashboard/… but NOT …/quox-dashboard-cli/… or …-wt-…).
100
+ const mutable = /-wt-/.test(fallback) || /\/quox-dashboard\//.test(fallback);
101
+ return { bin: fallback, source: 'install-time process path', mutable };
102
+ }
103
+
104
+ function loadNamedState(path, factory) {
105
+ try {
106
+ if (!existsSync(path)) return factory();
107
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
108
+ return { ...factory(), ...parsed };
109
+ } catch {
110
+ // A corrupt or unreadable state file must never crash the warden; it
111
+ // just loses continuity and starts fresh.
112
+ return factory();
113
+ }
114
+ }
115
+
116
+ function saveNamedState(path, state) {
117
+ ensureDirFor(path);
118
+ writeFileSync(path, JSON.stringify(state, null, 2));
119
+ }
120
+
121
+ function ensureDirFor(path) {
122
+ try { mkdirSync(dirname(path), { recursive: true }); } catch { /* already exists */ }
123
+ }
124
+
125
+ export function loadState(path) {
126
+ try {
127
+ if (!existsSync(path)) return createInitialState();
128
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
129
+ return { ...createInitialState(), ...parsed };
130
+ } catch {
131
+ // A corrupt or unreadable state file must never crash the runner; it
132
+ // just loses continuity and starts a fresh first-run seed.
133
+ return createInitialState();
134
+ }
135
+ }
136
+
137
+ export function saveState(path, state) {
138
+ ensureDirFor(path);
139
+ writeFileSync(path, JSON.stringify(state, null, 2));
140
+ }
141
+
142
+ function makeLogger(logPath) {
143
+ ensureDirFor(logPath);
144
+ return (level, message) => {
145
+ const line = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
146
+ try { appendFileSync(logPath, `${line}\n`); } catch { /* logging must never crash the runner */ }
147
+ if (level === 'warn') printWarning(message);
148
+ else if (level === 'error') console.error(message);
149
+ else printInfo(message);
150
+ };
151
+ }
152
+
153
+ /** Read the timestamp off the last line of the log file, if any. */
154
+ function lastLogActivity(logPath) {
155
+ try {
156
+ if (!existsSync(logPath)) return null;
157
+ const lines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
158
+ if (lines.length === 0) return null;
159
+ const match = lines[lines.length - 1].match(/^\[([^\]]+)\]/);
160
+ if (!match) return null;
161
+ const ts = new Date(match[1]).getTime();
162
+ return Number.isFinite(ts) ? ts : null;
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ function sleep(ms) {
169
+ return new Promise((resolve) => setTimeout(resolve, ms));
170
+ }
171
+
172
+ function collectRepeatable(value, previous) {
173
+ return (previous || []).concat([value]);
174
+ }
175
+
176
+ /**
177
+ * Resolve slot/host defaults from the resident bootstrap profile
178
+ * (~/.config/quox/resident-profile.json). Never throws for a missing
179
+ * profile: --host/--slot can supply both explicitly (e.g. an operator
180
+ * driving this from their own machine against a resident host).
181
+ */
182
+ function loadResidentProfileSafely(explicitPath) {
183
+ try {
184
+ return loadResidentProfile(explicitPath);
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ export function registerDevSupervisorCommands(dev, ctx) {
191
+ const { client, auth, program } = ctx;
192
+
193
+ const supervisor = dev
194
+ .command('supervisor')
195
+ .description('Headless runner: answer a resident\'s slot channel with no interactive session open');
196
+
197
+ // Split from the combined resolveTargets below (item 1a/1c, found by the
198
+ // acceptance owner driving this on a real host, 2026-08-23) so `install`
199
+ // can refuse cleanly on a missing slot alone, and `run --service` can
200
+ // idle instead of crash-looping on one, without either needing a host id
201
+ // check to also participate in that branching.
202
+ function resolveHostId(options) {
203
+ const profile = loadResidentProfileSafely(options.residentProfile);
204
+ const hostId = options.host || profile?.host_id || null;
205
+ if (!hostId) {
206
+ throw new Error('No host id: pass --host <hostId>, or provision a resident bootstrap profile with host_id.');
207
+ }
208
+ return hostId;
209
+ }
210
+
211
+ function resolveSlotName(options) {
212
+ const profile = loadResidentProfileSafely(options.residentProfile);
213
+ return options.slot || profile?.slot || null;
214
+ }
215
+
216
+ function resolveTargets(options) {
217
+ const hostId = resolveHostId(options);
218
+ const slotName = resolveSlotName(options);
219
+ if (!slotName) {
220
+ throw new Error('No slot: pass --slot <name>, or add "slot" to the resident bootstrap profile.');
221
+ }
222
+ return { hostId, slotName };
223
+ }
224
+
225
+ supervisor
226
+ .command('run')
227
+ .description('Poll the resident\'s slot channel and answer new owner messages headlessly')
228
+ .option('--slot <name>', 'slot to poll (default: resident bootstrap profile)')
229
+ .option('--host <hostId>', 'dev host_id (default: resident bootstrap profile)')
230
+ .option('--once', 'run a single poll pass and exit (what tests drive)')
231
+ .option('--service', 'Option A persistence: idle instead of exit when the kill switch is off, so a systemd unit never needs a restart to resume answering')
232
+ .option('--log <path>', 'log file path', defaultLogPath())
233
+ .option('--state <path>', 'continuity state file path', defaultStatePath())
234
+ .option('--resident-profile <path>', 'resident bootstrap profile path (default: ~/.config/quox/resident-profile.json)')
235
+ .option('--from <name>', 'accept messages only from this owner name (repeatable; default: anyone but this resident\'s own posts)', collectRepeatable, [])
236
+ .option('--history <n>', 'channel messages carried as continuity handover per turn', (v) => parseInt(v, 10), 10)
237
+ .option('--max-age-minutes <n>', 'never answer an owner message older than this, mark it seen instead (churn guard)', (v) => parseInt(v, 10), DEFAULT_MAX_MESSAGE_AGE_MINUTES)
238
+ .option('--pump-state <path>', 'warden pump continuity state file path (QUOXPERTICUS-REVISIT P4)', defaultPumpStatePath())
239
+ .option('--alarm-state <path>', 'warden alarm continuity state file path (QUOXPERTICUS-REVISIT P5)', defaultAlarmStatePath())
240
+ .action(async (options) => {
241
+ auth.requireAuth();
242
+ const hostId = resolveHostId(options);
243
+ let slotName = resolveSlotName(options);
244
+ const orgId = resolveOrgId(auth, program);
245
+ const selfIdentity = buildSelfIdentity(hostId);
246
+ const logPath = expandHome(options.log);
247
+ const statePath = expandHome(options.state);
248
+ const log = makeLogger(logPath);
249
+
250
+ let state = loadState(statePath);
251
+
252
+ const fetchSettings = async () => {
253
+ const ep = resolveEndpoint('v1.dev.supervisor.get', { hostId });
254
+ return client.get(ep.path, { service: ep.service });
255
+ };
256
+
257
+ if (!slotName) {
258
+ if (!options.service) {
259
+ throw new Error('No slot: pass --slot <name>, or add "slot" to the resident bootstrap profile.');
260
+ }
261
+ // LAUNCH BLOCKER FIX (item 1c, found by the acceptance owner driving
262
+ // this on a real host, 2026-08-23): before this fix, an unresolvable
263
+ // slot exited non-zero, and under systemd Restart=always that meant a
264
+ // busy-loop roughly every RestartSec forever. A --service runner
265
+ // with no slot yet has nothing to answer, but that is not a crash:
266
+ // idle on the same slow re-check cadence as the disabled-switch
267
+ // branch below, re-resolving from --slot/the bootstrap profile AND
268
+ // the server-side supervisor settings (whose `slot` field needs only
269
+ // hostId to fetch, set independently via `quox dev supervisor
270
+ // enable --slot`), until one appears.
271
+ log('warn', `startup: host=${hostId} no slot resolved (checked --slot and the resident bootstrap profile). service mode: idling (re-check every ${DEFAULT_IDLE_RECHECK_SECONDS}s) instead of exiting; no turns run with no slot to answer.`);
272
+ while (!slotName) {
273
+ if (options.once) {
274
+ log('info', 'shutdown');
275
+ return;
276
+ }
277
+ await sleep(DEFAULT_IDLE_RECHECK_SECONDS * 1000);
278
+ slotName = resolveSlotName(options);
279
+ if (!slotName) {
280
+ try {
281
+ const settings = resolveSettings(await fetchSettings());
282
+ if (settings.slot) slotName = settings.slot;
283
+ } catch (err) {
284
+ log('warn', `service mode: idle slot re-check failed: ${err.message}`);
285
+ }
286
+ }
287
+ }
288
+ log('info', `service mode: slot resolved (${slotName}); resuming without a restart.`);
289
+ }
290
+
291
+ log('info', `startup: host=${hostId} slot=${slotName} log=${logPath} state=${statePath} once=${!!options.once} service=${!!options.service}`);
292
+
293
+ const fetchMessages = async () => {
294
+ const ep = resolveEndpoint('v1.slots.messages', { name: slotName });
295
+ const data = await client.get(ep.path, { service: ep.service, query: { org_id: orgId, limit: 50 } });
296
+ return data.messages || [];
297
+ };
298
+ // Long-poll primitive, same contract as `quox slot wait --once`: one
299
+ // ~50s round trip, returns whatever landed since `cursor` (or nothing).
300
+ // Reused here rather than shelling out to the CLI verb itself, but it
301
+ // hits the exact same collector endpoint slot.js's `wait` command does.
302
+ // holder_session/exclude_from mirror `slot wait --session`: this is how
303
+ // this codebase keeps a slot's hold warm on every round trip, so the
304
+ // resident stays visibly present (not just "answering") for as long as
305
+ // it is actively watching.
306
+ const fetchWait = async (cursor) => {
307
+ const ep = resolveEndpoint('v1.slots.wait', { name: slotName });
308
+ const query = { org_id: orgId, timeout_ms: 50000, holder_session: selfIdentity, exclude_from: selfIdentity };
309
+ if (cursor) query.since = cursor;
310
+ return client.get(ep.path, { service: ep.service, query, timeout: 65000 });
311
+ };
312
+ const postReply = async (text) => {
313
+ const ep = resolveEndpoint('v1.slots.say', { name: slotName });
314
+ await client.post(ep.path, { service: ep.service, body: { text, from: selfIdentity, org_id: orgId } });
315
+ };
316
+ // Present-but-silent seam for the idle branch below: keeps the hold
317
+ // alive with NO cursor movement and NO message read, so nothing can be
318
+ // lost or skipped across a disable/re-enable transition.
319
+ const postHeartbeat = async () => {
320
+ const ep = resolveEndpoint('v1.slots.heartbeat', { name: slotName });
321
+ await client.post(ep.path, { service: ep.service, body: { holder_session: selfIdentity, org_id: orgId } });
322
+ };
323
+ // PRESENCE FIX (item 2, live outage 2026-08-23): both residents' slots
324
+ // sat FREE while their supervisors ran, because nothing ever called
325
+ // checkout, only heartbeat. This is a plain (non-force) checkout as
326
+ // this resident's own identity: the collector's checkout is a
327
+ // compare-and-swap on state='free', so it is safe to attempt on every
328
+ // poll pass. See attemptSlotClaim in supervisorEngine.js for how a
329
+ // 409 (already held by us, or held by someone else) is handled.
330
+ const checkoutSlot = async () => {
331
+ const ep = resolveEndpoint('v1.slots.checkout', { name: slotName });
332
+ return client.post(ep.path, {
333
+ service: ep.service,
334
+ body: { holder_session: selfIdentity, holder_kind: 'resident', host_id: hostId, org_id: orgId, force: false },
335
+ });
336
+ };
337
+ const claimState = createClaimState();
338
+ const spawnFn = ctx.spawnFn || spawn;
339
+ const spawnTurn = (prompt, timeoutSeconds) => runClaudeTurn({ spawnFn, prompt, timeoutSeconds });
340
+ // QP-DASH P1: report every attempted turn as a receipt. The engine
341
+ // wraps this fail-soft, so a collector outage never breaks answering.
342
+ const postTurnReceipt = async (fields) => {
343
+ const ep = resolveEndpoint('v1.dev.supervisor.turns.record', { hostId });
344
+ await client.post(ep.path, { service: ep.service, body: { ...fields, slot_name: slotName, org_id: orgId } });
345
+ };
346
+
347
+ // GRIPE-797: one owner alert per auth-expiry episode, same transport
348
+ // order as the warden alarm (tg-send.sh first, slot say fallback).
349
+ const alertOwner = async (text) => {
350
+ const script = ctx.tgSendScriptPath || join(homedir(), '.claude', 'skills', 'telegram-owner', 'tg-send.sh');
351
+ await sendAlarm({
352
+ tgSend: async (session, t) => { await (ctx.execFileAsync || promisify(execFile))('bash', [script, session, t]); },
353
+ postSlotNotice: postReply, text, sessionName: `SUPERVISOR-${hostId}`, log,
354
+ });
355
+ };
356
+ const commonPollArgs = {
357
+ alertOwner,
358
+ hostId,
359
+ slot: slotName,
360
+ selfIdentity,
361
+ fromFilters: options.from,
362
+ fetchSettings,
363
+ postReply,
364
+ spawnTurn,
365
+ checkoutSlot,
366
+ claimState,
367
+ postHeartbeat,
368
+ postTurnReceipt,
369
+ log,
370
+ historyWindow: options.history,
371
+ maxAgeMinutes: Number.isInteger(options.maxAgeMinutes) ? options.maxAgeMinutes : DEFAULT_MAX_MESSAGE_AGE_MINUTES,
372
+ };
373
+
374
+ // QUOXPERTICUS-REVISIT P4/P5: WARDEN MODE. When this host's slot is
375
+ // held by a FULL INTERACTIVE claude session on THIS host (the resident
376
+ // this process wards, deployed via `quox dev resident deploy`), the
377
+ // HOLD-IS-THE-VOICE gate inside runPollOnce already keeps this process
378
+ // structurally silent (see supervisorEngine.js). Rather than leave
379
+ // that silence unattended, this process becomes the WARDEN for that
380
+ // pass: it pumps new owner messages into the resident's live pane
381
+ // (residentPump.js) and watches the resident's health (residentAlarm.js)
382
+ // instead of running the normal answering pass at all.
383
+ const execFileAsync = ctx.execFileAsync || promisify(execFile);
384
+ const pumpStatePath = expandHome(options.pumpState || defaultPumpStatePath());
385
+ const alarmStatePath = expandHome(options.alarmState || defaultAlarmStatePath());
386
+ let pumpState = loadNamedState(pumpStatePath, createInitialPumpState);
387
+ let alarmState = loadNamedState(alarmStatePath, createInitialAlarmState);
388
+
389
+ const fetchSlotRow = async () => {
390
+ const ep = resolveEndpoint('v1.slots.list');
391
+ const data = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
392
+ return ((data && data.slots) || []).find((s) => s.name === slotName) || null;
393
+ };
394
+
395
+ // Runs ONE warden cycle (pump + watchdog) when applicable. Returns
396
+ // {wasWarden:false} when the slot is not held by an interactive
397
+ // resident on this host, in which case the caller falls through to
398
+ // the normal headless answering pass, completely unchanged. Never
399
+ // throws: a slot lookup failure is logged and treated as "not warded
400
+ // this pass", never as a reason to stop the runner.
401
+ async function runWardenCycle() {
402
+ let slotRow;
403
+ try {
404
+ slotRow = await fetchSlotRow();
405
+ } catch (err) {
406
+ log('warn', `warden: could not read slot ${slotName} to check for an interactive hold: ${err.message}`);
407
+ return { wasWarden: false };
408
+ }
409
+ const isWardedResident = !!(
410
+ slotRow && slotRow.state === 'held' && slotRow.holder_kind === 'interactive' && slotRow.host_id === hostId
411
+ );
412
+ if (!isWardedResident) return { wasWarden: false };
413
+
414
+ const residentSessionId = slotRow.holder_session;
415
+ // Default naming convention from `quox dev resident deploy`
416
+ // (lib/commands/devResident.js defaultSessionName): a resident
417
+ // deployed with a custom --session-name is not matched by this.
418
+ const sessionName = `resident-${slotName}`;
419
+
420
+ const readPane = async () => {
421
+ const ep = resolveEndpoint('v1.dev.sessions.pane', { hostId, sessionId: residentSessionId });
422
+ const data = await client.get(ep.path, { service: ep.service });
423
+ return (data && data.pane) || '';
424
+ };
425
+ const injectKeys = async (line) => {
426
+ const ep = resolveEndpoint('v1.dev.sessions.keys', { hostId, sessionId: residentSessionId });
427
+ await client.post(ep.path, { service: ep.service, body: { keys: line } });
428
+ };
429
+ const heartbeatHold = async () => {
430
+ const ep = resolveEndpoint('v1.slots.heartbeat', { name: slotName });
431
+ await client.post(ep.path, { service: ep.service, body: { holder_session: residentSessionId, org_id: orgId } });
432
+ };
433
+
434
+ try {
435
+ const pumpResult = await runPumpOnce({
436
+ hostId, slot: slotName, selfIdentity, fromFilters: options.from,
437
+ fetchMessages, readPane, injectKeys, heartbeatHold, postReply,
438
+ state: pumpState, log,
439
+ });
440
+ pumpState = pumpResult.state;
441
+ saveNamedState(pumpStatePath, pumpState);
442
+ } catch (err) {
443
+ log('warn', `warden: pump pass failed (never blocks the watchdog below): ${err.message}`);
444
+ }
445
+
446
+ const listSessions = async () => {
447
+ const ep = resolveEndpoint('v1.dev.sessions.list', { hostId });
448
+ const data = await client.get(ep.path, { service: ep.service });
449
+ return (data && data.sessions) || [];
450
+ };
451
+ const releaseSlot = async () => {
452
+ const ep = resolveEndpoint('v1.slots.release', { name: slotName });
453
+ await client.post(ep.path, { service: ep.service, body: { org_id: orgId, force: true } });
454
+ };
455
+ // Reuses `quox dev resident deploy` (lib/commands/devResident.js)
456
+ // via a child process rather than importing its internals: deploy's
457
+ // closures are private to registerDevResidentCommands, and shelling
458
+ // out exercises the exact same governed path a human would run.
459
+ const redeploy = async () => {
460
+ const quoxBin = ctx.quoxBin || process.argv[1] || 'quox';
461
+ await execFileAsync(quoxBin, ['dev', 'resident', 'deploy', '--host', hostId, '--slot', slotName, '--force']);
462
+ };
463
+ const claimFallback = async () => attemptSlotClaim({ checkoutSlot, selfIdentity, slot: slotName, log, announceForeignHold: false, claimState, postHeartbeat });
464
+ const tgSend = async (session, text) => {
465
+ const script = ctx.tgSendScriptPath || join(homedir(), '.claude', 'skills', 'telegram-owner', 'tg-send.sh');
466
+ await execFileAsync('bash', [script, session, text]);
467
+ };
468
+
469
+ try {
470
+ const alarmResult = await runWardenPass({
471
+ hostId, slot: slotName, sessionName,
472
+ listSessions, readPane, releaseSlot, redeploy, claimFallback,
473
+ postSlotNotice: postReply, recordTurn: postTurnReceipt, tgSend,
474
+ stuckDialogSinceMs: pumpState.stuck_dialog_since,
475
+ state: alarmState, log,
476
+ });
477
+ alarmState = alarmResult.state;
478
+ saveNamedState(alarmStatePath, alarmState);
479
+ } catch (err) {
480
+ log('warn', `warden: watchdog pass failed: ${err.message}`);
481
+ }
482
+
483
+ return { wasWarden: true };
484
+ }
485
+
486
+ // Watching strategy for a continuous (non --once) run: long-poll via
487
+ // `wait` is the primary path (requirement: no sleep loop watching for
488
+ // messages). The very first pass of a continuous run still uses the
489
+ // classic /messages fetch, unchanged, so first-run backlog seeding
490
+ // stays exactly what it always was; every pass after that switches to
491
+ // waitMode and re-waits from the cursor the previous round returned.
492
+ // If wait fails DEFAULT_WAIT_FAILURE_THRESHOLD times in a row, this
493
+ // permanently falls back to the original interval-poll loop for the
494
+ // rest of THIS process's life: degrade, never die.
495
+ const continuous = !options.once;
496
+ let running = true;
497
+ let pollSeconds = DEFAULT_SETTINGS.poll_seconds;
498
+ let waitMode = false;
499
+ let waitDisabled = false;
500
+ let waitFailures = 0;
501
+ let cursor = null;
502
+ // Service mode idling: set when the server-side switch is off and
503
+ // --service was given.
504
+ //
505
+ // PRESENT BUT SILENT (deliberate, on the record): while idling, this
506
+ // resident keeps holding its slot and keeps heartbeating it, so it
507
+ // reads as a live host that is not currently permitted to act, never
508
+ // as an absent one. It answers NOTHING while idling: no message is
509
+ // fetched for processing here (only the heartbeat, which reads
510
+ // nothing) and no turn is ever spawned. Only the server-side switch
511
+ // itself is re-checked, slowly.
512
+ let idling = false;
513
+
514
+ // LOOP TRIAGE FIX (diagnosed live 2026-09-10, .131): before this, the
515
+ // ENABLED branch's only heartbeat was the `slot wait` long-poll's own
516
+ // server-side coupling (holder_session on every wait round trip). An
517
+ // intermittent hang on the authenticated slot path (observed:
518
+ // "checkout attempt failed: Request timed out after 30000ms" while
519
+ // /health stayed instant, so this is a server-side hang, not network)
520
+ // wedged the long-poll, and NO heartbeat fired until it recovered, so
521
+ // the hold lapsed stale (15m TTL) then free (30m) and re-claimed
522
+ // ~45 minutes later, ~193 times over 6 days. This independent
523
+ // heartbeat runs on its own fixed-interval timer (see
524
+ // startIndependentHeartbeat in supervisorEngine.js), decoupled from
525
+ // the wait long-poll, a turn spawn, or the idle branch's own recheck
526
+ // cadence, so it keeps the hold warm through all of them. It never
527
+ // seizes anything (postHeartbeat only warms a hold this resident
528
+ // already has; a 409 is logged and left alone, same rule as
529
+ // everywhere else in this file).
530
+ const independentHeartbeat = startIndependentHeartbeat({
531
+ postHeartbeat,
532
+ intervalMs: DEFAULT_HEARTBEAT_INTERVAL_SECONDS * 1000,
533
+ log,
534
+ });
535
+
536
+ try {
537
+ while (running) {
538
+ if (idling) {
539
+ // PRESENCE FIX (item 2, live outage 2026-08-23): claim the slot
540
+ // here too, before the heartbeat below, so idling reads as PRESENT
541
+ // rather than present-but-silent-and-not-even-holding-it. Never
542
+ // logs the foreign-hold case itself (announceForeignHold: false):
543
+ // the existing heartbeat 409 handler right below already reports
544
+ // that, unchanged, and would otherwise double-log the same event.
545
+ const idleClaim = await attemptSlotClaim({ checkoutSlot, selfIdentity, slot: slotName, log, announceForeignHold: false, claimState, postHeartbeat });
546
+ // HOLD-IS-THE-VOICE gate, mirrored (QUOXPERTICUS-REVISIT P2): this
547
+ // idle loop never fetches messages or spawns turns regardless (the
548
+ // kill switch is off here), but a foreign hold is still witnessed
549
+ // with the same once-per-holder-per-hour receipt the engine posts
550
+ // on an enabled pass, so "who held it and when" is legible even
551
+ // while service mode is idling on the disabled switch.
552
+ if (!idleClaim.claimed && idleClaim.reason === 'held_by_other') {
553
+ const holder = idleClaim.holder || 'unknown';
554
+ const notice = shouldPostErrorNotice(state.held_by_other_notices || [], holder, Date.now());
555
+ state = { ...state, held_by_other_notices: notice.notices };
556
+ if (notice.shouldPost) {
557
+ try {
558
+ await postTurnReceipt({ outcome: 'silent_held_by_other', held_by: holder });
559
+ } catch (err) {
560
+ log('warn', `silent-held receipt failed (never blocks idling): ${err.message}`);
561
+ }
562
+ }
563
+ saveState(statePath, state);
564
+ }
565
+ try {
566
+ await postHeartbeat();
567
+ } catch (err) {
568
+ // SLOT OWNERSHIP INTENT (item 3, found by the acceptance owner
569
+ // driving this on a real host, 2026-08-23): this heartbeat is
570
+ // read-only warmth on a hold this resident already has, never a
571
+ // seize. If another session (human or resident) already holds
572
+ // ${slotName}, the collector's heartbeatSlot() correctly refuses
573
+ // (409, "not held by this session") rather than handing the hold
574
+ // over silently. Reporting that as a generic failure buries
575
+ // exactly the information an operator needs; call it what it is.
576
+ // Silently seizing a hold someone else deliberately owns is the
577
+ // same failure class as the checkoutSlot assigned_to bug fixed
578
+ // earlier tonight (services/collector/lib/devSlotsStore.js),
579
+ // where an assignment was ignored and a slot could be taken out
580
+ // from under its owner — this is the CLI-side mirror of that
581
+ // rule: never force a hold this process does not already have.
582
+ if (err && err.status === 409) {
583
+ log('warn', `slot ${slotName}: idle heartbeat got 409 (not held by this resident) — leaving it alone rather than seizing it. Run \`quox dev supervisor status\` to see who holds it.`);
584
+ } else {
585
+ log('warn', `service mode: idle heartbeat failed: ${err.message}`);
586
+ }
587
+ }
588
+ await sleep(DEFAULT_IDLE_RECHECK_SECONDS * 1000);
589
+ let settings;
590
+ try {
591
+ settings = resolveSettings(await fetchSettings());
592
+ } catch (err) {
593
+ log('warn', `service mode: idle re-check failed: ${err.message}`);
594
+ continue;
595
+ }
596
+ if (!settings.enabled) continue;
597
+ idling = false;
598
+ log('info', 'service mode: supervisor re-enabled, resuming without a restart.');
599
+ // Falls through into a normal pass below, same iteration.
600
+ }
601
+
602
+ // QUOXPERTICUS-REVISIT P4/P5: check for warden mode BEFORE the
603
+ // normal answering pass below. When the slot is held by an
604
+ // interactive resident on this host, this pass pumps + watches
605
+ // instead of running runPollOnce at all (which would find the same
606
+ // hold and just sit silent via the HOLD-IS-THE-VOICE gate). Skipped
607
+ // while idling above (that branch already has its own foreign-hold
608
+ // handling and re-checks the kill switch first).
609
+ if (!idling) {
610
+ let wardenOutcome = { wasWarden: false };
611
+ try {
612
+ wardenOutcome = await runWardenCycle();
613
+ } catch (err) {
614
+ log('warn', `warden cycle failed unexpectedly (falling through to a normal pass): ${err.message}`);
615
+ }
616
+ if (wardenOutcome.wasWarden) {
617
+ if (options.once) break;
618
+ await sleep(pollSeconds * 1000);
619
+ continue;
620
+ }
621
+ }
622
+
623
+ let result = null;
624
+ try {
625
+ if (continuous && waitMode) {
626
+ const waitData = await fetchWait(cursor);
627
+ cursor = waitData.cursor || cursor;
628
+ waitFailures = 0;
629
+ const waitMessages = waitData.messages || [];
630
+ result = await runPollOnce({ ...commonPollArgs, state, fetchMessages: async () => waitMessages });
631
+ } else {
632
+ result = await runPollOnce({ ...commonPollArgs, state, fetchMessages });
633
+ // Only switch to wait-mode off a pass that actually ran enabled
634
+ // (result.stopped false): a classic pass returned early by the
635
+ // kill switch never touched state.bootstrapped or seen_ids, so
636
+ // treating it as the "first pass seeded the backlog" moment
637
+ // would hand wait a bare cursor and skip the real backlog seed
638
+ // the first ever enabled pass is required to do.
639
+ if (continuous && !waitDisabled && result && !result.stopped) {
640
+ cursor = new Date().toISOString();
641
+ waitMode = true;
642
+ }
643
+ }
644
+ } catch (err) {
645
+ log('error', `poll failed: ${err.message}`);
646
+ // #1018: a single pass that failed is a failed run, not a quiet success.
647
+ if (options.once) process.exitCode = 1;
648
+ if (continuous && waitMode) {
649
+ waitFailures += 1;
650
+ if (waitFailures >= DEFAULT_WAIT_FAILURE_THRESHOLD) {
651
+ waitMode = false;
652
+ waitDisabled = true;
653
+ waitFailures = 0;
654
+ log('warn', `wait failed ${DEFAULT_WAIT_FAILURE_THRESHOLD} times in a row; falling back to interval polling every ${pollSeconds}s.`);
655
+ }
656
+ }
657
+ }
658
+
659
+ if (result) {
660
+ state = result.state;
661
+ saveState(statePath, state);
662
+ pollSeconds = result.settings.poll_seconds;
663
+ if (result.stopped) {
664
+ if (options.service && !options.once) {
665
+ idling = true;
666
+ log('info', `service mode: idling (re-check every ${DEFAULT_IDLE_RECHECK_SECONDS}s) until re-enabled; no turns run while disabled.`);
667
+ } else {
668
+ running = false;
669
+ }
670
+ }
671
+ }
672
+
673
+ if (options.once) break;
674
+ if (running && !idling && !waitMode) await sleep(pollSeconds * 1000);
675
+ }
676
+ } finally {
677
+ independentHeartbeat.stop();
678
+ }
679
+
680
+ log('info', 'shutdown');
681
+ });
682
+
683
+ // quox#470: the switch had a dashboard toggle but no CLI verb, so an org
684
+ // owner at a terminal had no way to flip it and it read as unreachable.
685
+ // Server-side this is requireHumanAdmin, so an org owner/admin passes and
686
+ // a resident's own agent-scoped token is refused however it is called.
687
+ function registerSwitch(name, enabled, description) {
688
+ supervisor
689
+ .command(name)
690
+ .description(description)
691
+ .option('--host <hostId>', 'dev host_id (default: resident bootstrap profile)')
692
+ .option('--slot <name>', 'slot the runner should answer (default: resident bootstrap profile)')
693
+ .option('--poll-seconds <n>', 'seconds between polls (5-300)', (v) => parseInt(v, 10))
694
+ .option('--turn-timeout-seconds <n>', 'per-turn timeout (30-600)', (v) => parseInt(v, 10))
695
+ .option('--max-turns-per-hour <n>', 'rate cap (1-120)', (v) => parseInt(v, 10))
696
+ .option('--resident-profile <path>', 'resident bootstrap profile path')
697
+ .action(async (options) => {
698
+ auth.requireAuth();
699
+ const profile = loadResidentProfileSafely(options.residentProfile);
700
+ const hostId = options.host || profile?.host_id || null;
701
+ if (!hostId) {
702
+ throw new Error('No host id: pass --host <hostId>, or run where a resident bootstrap profile exists.');
703
+ }
704
+ const body = { enabled };
705
+ // Only send a slot when enabling: disabling should never need one,
706
+ // and sending undefined would clobber the stored value.
707
+ const slotName = options.slot || profile?.slot || null;
708
+ if (enabled && slotName) body.slot = slotName;
709
+ if (Number.isInteger(options.pollSeconds)) body.poll_seconds = options.pollSeconds;
710
+ if (Number.isInteger(options.turnTimeoutSeconds)) body.turn_timeout_seconds = options.turnTimeoutSeconds;
711
+ if (Number.isInteger(options.maxTurnsPerHour)) body.max_turns_per_hour = options.maxTurnsPerHour;
712
+
713
+ const ep = resolveEndpoint('v1.dev.supervisor.set', { hostId });
714
+ const data = await client.put(ep.path, { service: ep.service, body });
715
+ formatDetail(data, { format: program.opts().output, title: `Supervisor ${enabled ? 'enabled' : 'disabled'} for ${hostId}` });
716
+ printSuccess(
717
+ enabled
718
+ ? `Supervisor enabled for ${hostId}. Start the runner on that host: quox dev supervisor run`
719
+ : `Supervisor disabled for ${hostId}. A running runner exits within one poll.`
720
+ );
721
+ });
722
+ }
723
+
724
+ registerSwitch('enable', true, 'Turn the headless supervisor ON for a host (human admin only)');
725
+ registerSwitch('disable', false, 'Turn the headless supervisor OFF for a host (kill switch)');
726
+
727
+ // Option A persistence, delivery half. Installing this unit is deliberately
728
+ // NOT the autonomy grant: see the governance note at the top of
729
+ // ../core/systemdInstaller.js. The unit only ever answers a message when
730
+ // the human-gated server-side switch above is on, so a resident installing
731
+ // its own service on its own host (the scoped self-service pattern) still
732
+ // leaves the "may it act" decision with a human.
733
+ supervisor
734
+ .command('install')
735
+ .description('Install a systemd --user unit running this supervisor headlessly (Restart=always; survives logout/reboot with linger)')
736
+ .option('--host <hostId>', 'baked into the unit only if given; otherwise the runner resolves it from the resident bootstrap profile at start')
737
+ .option('--slot <name>', 'baked into the unit only if given; otherwise resolved from the resident bootstrap profile at start')
738
+ .option('--resident-profile <path>', 'baked into the unit only if given (non-default profile path)')
739
+ .option('--quox-bin <path>', 'path to the quox executable the unit runs (default: the stable CLI worktree ~/quox-dashboard-cli, else $QUOX_CLI_DIR, else this process\'s path with a mutable-tree warning — #619)')
740
+ .option('--restart-sec <n>', 'systemd RestartSec for the unit', (v) => parseInt(v, 10), DEFAULT_RESTART_SEC)
741
+ .option('--activate-now', 'acknowledge that the switch is already ON, so installing starts an autonomous responder immediately')
742
+ .action(async (options) => {
743
+ auth.requireAuth();
744
+
745
+ // GOVERNANCE SEQUENCING (found by the acceptance owner driving this on
746
+ // a real host, 2026-08-23). "Install does not grant autonomy" is true
747
+ // only while the supervisor switch is OFF: installing then produces a
748
+ // present-but-silent unit. If the switch is already ON, installing
749
+ // brings an autonomous responder live in the same motion, so the act
750
+ // of installing IS the activation. Their safety classifier correctly
751
+ // refused to let them do that, and the boundary should live in the
752
+ // tool rather than only in a classifier that constrains AI callers.
753
+ //
754
+ // So: refuse a plain install while enabled, and name both safe
755
+ // sequences. --activate-now is the deliberate acknowledgement for a
756
+ // human who genuinely means "install and go live right now".
757
+ try {
758
+ const probeHost = options.host || loadResidentProfileSafely(options.residentProfile)?.host_id || null;
759
+ if (probeHost && !options.activateNow) {
760
+ const ep = resolveEndpoint('v1.dev.supervisor.get', { hostId: probeHost });
761
+ const current = resolveSettings(await client.get(ep.path, { service: ep.service }));
762
+ if (current.enabled) {
763
+ throw new Error(
764
+ `Supervisor is already ENABLED for ${probeHost}, so installing now would start an autonomous responder immediately, not a silent one.\n` +
765
+ `Either:\n` +
766
+ ` disable it, install (present but silent), then have a human re-enable it, or\n` +
767
+ ` have a human run this install with --activate-now to activate deliberately.\n` +
768
+ `Turning autonomy on is always a human action by design.`
769
+ );
770
+ }
771
+ }
772
+ } catch (err) {
773
+ // A settings lookup that genuinely failed must not silently become
774
+ // permission to install into an unknown state.
775
+ if (/already ENABLED/.test(err.message)) throw err;
776
+ printWarning(`Could not confirm the supervisor switch state before installing: ${err.message}`);
777
+ }
778
+
779
+ // LAUNCH BLOCKER FIX (item 1a, found by the acceptance owner driving
780
+ // this on a real host, 2026-08-23): a unit installed with no slot
781
+ // resolvable dies at startup ("No slot: pass --slot") and, under
782
+ // Restart=always, busy-loops roughly every RestartSec forever, which
783
+ // hits every freshly dev-enabled host until re-enable happens to
784
+ // carry a slot. Resolve the same way `run` does (--slot, else the
785
+ // resident bootstrap profile) and refuse to write anything at all if
786
+ // neither supplies one: never install a unit known to crash on start.
787
+ const resolvedInstallSlot = resolveSlotName(options);
788
+ if (!resolvedInstallSlot) {
789
+ throw new Error(
790
+ 'Cannot install: no slot resolved (checked --slot and the resident bootstrap profile).\n' +
791
+ 'A unit with no slot dies at startup and busy-loops under systemd Restart=always.\n' +
792
+ 'Either pass --slot <name>, or re-run `quox dev enable` once the resident bootstrap profile carries a slot.'
793
+ );
794
+ }
795
+
796
+ const extraArgs = [];
797
+ if (options.host) extraArgs.push('--host', options.host);
798
+ if (options.slot) extraArgs.push('--slot', options.slot);
799
+ if (options.residentProfile) extraArgs.push('--resident-profile', options.residentProfile);
800
+
801
+ // #619: prefer the STABLE CLI worktree over the install-time process
802
+ // path. Baking process.argv[1] into a Restart=always unit means the unit
803
+ // execs from whatever tree happened to run the install — a mutable dev
804
+ // tree or session worktree there is a delayed bomb (a resident sat dark
805
+ // ~15 days after its tree branch-switched out from under it).
806
+ const { bin: quoxBin, source: binSource, mutable: binMutable } = resolveSupervisorBin({
807
+ quoxBin: options.quoxBin,
808
+ argv1: process.argv[1],
809
+ home: homedir(),
810
+ });
811
+ if (binMutable) {
812
+ printWarning(
813
+ `Unit ExecStart will point at a MUTABLE tree: ${quoxBin}. A long-lived Restart=always unit `
814
+ + `must NOT exec from a live working tree — a branch switch or reset there silently changes or `
815
+ + `removes the deployed supervisor code (#619: a resident crash-looped to dark ~15 days this way). `
816
+ + `Install from the stable CLI worktree (~/quox-dashboard-cli), set $QUOX_CLI_DIR, or pass `
817
+ + `--quox-bin <stable path>. quox-sentry now alarms if the unit later goes dark, but pinning is the fix.`
818
+ );
819
+ } else {
820
+ printInfo(`Unit ExecStart bin: ${quoxBin} (${binSource}).`);
821
+ }
822
+
823
+ // BAKE THE ENVIRONMENT INTO THE UNIT (item 1, live outage 2026-08-23):
824
+ // a systemd --user unit inherits none of the shell environment that
825
+ // reach-back URL resolution (resolveResidentBaseUrl) leans on. On a
826
+ // LAN install the instance edge is self-signed, so every re-auth died
827
+ // with a bare "fetch failed", the supervisor logged "Session expired"
828
+ // every 30s, and both residents silently lost their slots. Derive the
829
+ // fix from the same bootstrap profile the resident already trusts for
830
+ // its own identity, so there is nothing new to keep in sync.
831
+ const installProfile = loadResidentProfileSafely(options.residentProfile);
832
+ const env = deriveSupervisorEnv(installProfile);
833
+ if (!installProfile) {
834
+ printWarning(
835
+ 'No resident bootstrap profile found: the unit is being installed with no baked-in environment. '
836
+ + 'If this host\'s instance edge is self-signed, re-auth may fail with "fetch failed". '
837
+ + 'Provision a bootstrap profile (or pass --resident-profile) and re-run install to fix this.'
838
+ );
839
+ } else if (Object.keys(env).length === 0) {
840
+ printWarning(
841
+ `Resident bootstrap profile at ${installProfile._path} has no instance_url: the unit is being installed with no baked-in environment. `
842
+ + 'If this host\'s instance edge is self-signed, re-auth may fail with "fetch failed".'
843
+ );
844
+ } else {
845
+ printInfo(`Unit will carry: ${Object.keys(env).join(', ')} (derived from the resident bootstrap profile).`);
846
+ }
847
+ const unitContent = buildUnitFile({ quoxBin, extraArgs, restartSec: options.restartSec, env });
848
+ const unitPath = ctx.unitPath || defaultUnitPath();
849
+ const unitDir = ctx.unitDir || defaultUnitDir();
850
+ const execFn = ctx.execFn || realExecFn;
851
+ const writeFileFn = ctx.writeFileFn || writeFileSync;
852
+ const mkdirFn = ctx.mkdirFn || mkdirSync;
853
+ const user = ctx.osUser || userInfo().username;
854
+ const sleepFn = ctx.sleepFn || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
855
+
856
+ const report = await installSupervisorService({ execFn, writeFileFn, mkdirFn, unitPath, unitDir, unitContent, user, sleepFn });
857
+
858
+ formatDetail({ ...report, unit_content: unitContent }, { format: program.opts().output, title: `Resident supervisor service (${SERVICE_NAME})` });
859
+
860
+ // ITEM 2 (found by the acceptance owner driving this on a real host,
861
+ // 2026-08-23): "enable --now" succeeding only proves systemd accepted
862
+ // the unit, never that it stayed up. A crash loop must be reported as
863
+ // a FAILURE, with the last journal lines when they can be read, never
864
+ // dressed up as the success block below.
865
+ if (report.crash_loop) {
866
+ printWarning(
867
+ `FAILURE: ${SERVICE_NAME} was enabled but is crash-looping ` +
868
+ `(active=${report.active}, restarts=${report.restart_count ?? 'unknown'}). It is answering nothing.`
869
+ );
870
+ if (report.crash_journal) {
871
+ printWarning(`Last journal lines:\n${report.crash_journal}`);
872
+ } else {
873
+ printWarning('Could not read journal lines (journalctl unavailable or restricted).');
874
+ }
875
+ } else if (report.enabled) {
876
+ printSuccess(`Installed and started ${SERVICE_NAME} (systemd --user, Restart=always). Verified it stayed up (active=${report.active}, restarts=${report.restart_count ?? 0}).`);
877
+ } else {
878
+ printWarning(`Unit written to ${unitPath} but \`systemctl --user enable --now\` failed: ${report.enable_error}`);
879
+ }
880
+
881
+ if (report.linger_enabled) {
882
+ printSuccess(`Linger enabled for ${user}: the service survives logout and reboot.`);
883
+ } else {
884
+ printWarning(
885
+ `Could not enable linger for ${user} (${report.linger_error}). Without linger, this unit STOPS on logout ` +
886
+ `and will NOT come back on its own after a reboot; it only resumes at the next interactive login. ` +
887
+ `Ask an administrator with sudo to run: loginctl enable-linger ${user}`
888
+ );
889
+ }
890
+ });
891
+
892
+ supervisor
893
+ .command('uninstall')
894
+ .description('Disable, stop, and remove the systemd --user unit')
895
+ .action(async () => {
896
+ auth.requireAuth();
897
+ const unitPath = ctx.unitPath || defaultUnitPath();
898
+ const execFn = ctx.execFn || realExecFn;
899
+ const existsFn = ctx.existsFn || existsSync;
900
+ const unlinkFn = ctx.unlinkFn || unlinkSync;
901
+
902
+ const report = await uninstallSupervisorService({ execFn, existsFn, unlinkFn, unitPath });
903
+ formatDetail(report, { format: program.opts().output, title: `Resident supervisor service removed (${SERVICE_NAME})` });
904
+ if (report.disabled && report.unit_removed) {
905
+ printSuccess(`${SERVICE_NAME} disabled, stopped, and removed.`);
906
+ } else {
907
+ printWarning(`Uninstall incomplete: disabled=${report.disabled} (${report.disable_error || 'ok'}), unit_removed=${report.unit_removed}.`);
908
+ }
909
+ });
910
+
911
+ supervisor
912
+ .command('turns')
913
+ .description('List per-turn receipts (what the headless resident spent, per message)')
914
+ .option('--host <hostId>', 'dev host_id (default: resident bootstrap profile)')
915
+ .option('--since <ts>', 'only receipts after this ISO-8601 timestamp')
916
+ .option('--limit <n>', 'max receipts (default 50)', (v) => parseInt(v, 10))
917
+ .action(async (options) => {
918
+ auth.requireAuth();
919
+ const hostId = resolveHostId(options);
920
+ const ep = resolveEndpoint('v1.dev.supervisor.turns.list', { hostId });
921
+ const query = {};
922
+ if (options.since) query.since = options.since;
923
+ if (options.limit) query.limit = options.limit;
924
+ const data = await client.get(ep.path, { service: ep.service, query });
925
+ const s = data.summary || {};
926
+ printInfo(`${hostId}: ${s.turns ?? 0} turn(s), ${s.replied ?? 0} replied, total $${(s.total_cost_usd ?? 0).toFixed(4)}`);
927
+ formatOutput((data.turns || []).map((t) => ({
928
+ TIME: t.created_at, OUTCOME: t.outcome, FROM: t.message_from || '-',
929
+ CHARS: t.reply_chars ?? '-', COST: t.cost_usd != null ? `$${t.cost_usd.toFixed(4)}` : '-',
930
+ MS: t.duration_ms ?? '-', NOTE: t.error_note || '',
931
+ })), {
932
+ format: program.opts().output,
933
+ columns: ['TIME', 'OUTCOME', 'FROM', 'CHARS', 'COST', 'MS', 'NOTE'],
934
+ emptyMessage: 'No turn receipts yet.',
935
+ raw: data.turns || [],
936
+ });
937
+ });
938
+
939
+ supervisor
940
+ .command('status')
941
+ .description('Show resolved supervisor settings and whether a runner appears live')
942
+ .option('--host <hostId>', 'dev host_id (default: resident bootstrap profile)')
943
+ .option('--slot <name>', 'slot (default: resident bootstrap profile)')
944
+ .option('--log <path>', 'log file path', defaultLogPath())
945
+ .option('--state <path>', 'continuity state file path', defaultStatePath())
946
+ .option('--resident-profile <path>', 'resident bootstrap profile path')
947
+ .action(async (options) => {
948
+ auth.requireAuth();
949
+ const { hostId, slotName } = resolveTargets(options);
950
+ const orgId = resolveOrgId(auth, program);
951
+ const selfIdentity = buildSelfIdentity(hostId);
952
+ const ep = resolveEndpoint('v1.dev.supervisor.get', { hostId });
953
+ const raw = await client.get(ep.path, { service: ep.service });
954
+ const settings = resolveSettings(raw);
955
+
956
+ // SLOT OWNERSHIP INTENT (item 3, found by the acceptance owner driving
957
+ // this on a real host, 2026-08-23): a foreign hold must be visible
958
+ // here, not just buried in the runner's log — this is how an operator
959
+ // learns why a running, correctly-installed supervisor is nonetheless
960
+ // not answering. Read-only lookup, best-effort: never let a slot-list
961
+ // failure break the rest of `status`. See the idle-heartbeat comment
962
+ // in `run` for the full rationale (never seize a hold another session
963
+ // deliberately owns; same failure class as the checkoutSlot
964
+ // assigned_to bug fixed earlier tonight).
965
+ // FIX 3 (presence must be legible, live outage 2026-08-23): status
966
+ // must say plainly whether THIS resident currently holds its slot,
967
+ // not just name a foreign holder when there is one. Same read-only,
968
+ // best-effort lookup as the foreign-hold check below.
969
+ let foreignHolder = null;
970
+ let heldBySelf = false;
971
+ try {
972
+ const listEp = resolveEndpoint('v1.slots.list');
973
+ const listData = await client.get(listEp.path, { service: listEp.service, query: { org_id: orgId } });
974
+ const slotRow = (listData.slots || []).find((s) => s.name === slotName);
975
+ if (slotRow && slotRow.state === 'held' && slotRow.holder_session) {
976
+ if (slotRow.holder_session === selfIdentity) heldBySelf = true;
977
+ else foreignHolder = slotRow.holder_session;
978
+ }
979
+ } catch (_err) {
980
+ // Best-effort; the rest of `status` still reports.
981
+ }
982
+
983
+ const logPath = expandHome(options.log);
984
+ const statePath = expandHome(options.state);
985
+ const state = loadState(statePath);
986
+ const lastActivity = lastLogActivity(logPath);
987
+ const staleAfterMs = Math.max(settings.poll_seconds * 3, 120) * 1000;
988
+ const live = lastActivity != null && (Date.now() - lastActivity) < staleAfterMs;
989
+
990
+ // Option A persistence: whether the systemd --user unit itself is
991
+ // installed/enabled/active/lingering. Read-only, same as everything
992
+ // else in this command; installing/removing is `install`/`uninstall`.
993
+ let service = { installed: false, enabled: false, active: false, linger: false };
994
+ try {
995
+ const execFn = ctx.execFn || realExecFn;
996
+ const existsFn = ctx.existsFn || existsSync;
997
+ const unitPath = ctx.unitPath || defaultUnitPath();
998
+ const user = ctx.osUser || userInfo().username;
999
+ service = await getServiceInstallStatus({ execFn, existsFn, unitPath, user });
1000
+ } catch (err) {
1001
+ service = { installed: false, enabled: false, active: false, linger: false, error: err.message };
1002
+ }
1003
+
1004
+ const report = {
1005
+ host_id: hostId,
1006
+ enabled: settings.enabled,
1007
+ slot: settings.slot,
1008
+ poll_seconds: settings.poll_seconds,
1009
+ turn_timeout_seconds: settings.turn_timeout_seconds,
1010
+ max_turns_per_hour: settings.max_turns_per_hour,
1011
+ updated_at: settings.updated_at,
1012
+ updated_by: settings.updated_by,
1013
+ runner_live: live,
1014
+ last_log_activity: lastActivity ? new Date(lastActivity).toISOString() : null,
1015
+ state_bootstrapped: state.bootstrapped,
1016
+ state_decisions_recorded: (state.decisions || []).length,
1017
+ state_updated_at: state.updated_at,
1018
+ // Defect 2 fix: the last error-shaped turn (never posted to the
1019
+ // channel), so an operator can see WHY nothing came through without
1020
+ // reading the log file.
1021
+ last_turn_error: state.last_turn_error
1022
+ ? `${state.last_turn_error.category} (${state.last_turn_error.type}) at ${state.last_turn_error.at}`
1023
+ : null,
1024
+ service_installed: service.installed,
1025
+ service_enabled: service.enabled,
1026
+ service_active: service.active,
1027
+ service_linger: service.linger,
1028
+ slot_held_by_self: heldBySelf,
1029
+ slot_held_by_other: foreignHolder,
1030
+ };
1031
+
1032
+ formatDetail(report, { format: program.opts().output, title: `Supervisor status: ${hostId}` });
1033
+ if (!settings.enabled) {
1034
+ printWarning(`Supervisor is disabled for ${hostId} (server-side switch). \`quox dev supervisor run\` will exit immediately.`);
1035
+ } else if (!live) {
1036
+ printWarning(`No recent runner activity in ${logPath}. Is \`quox dev supervisor run\` actually running?`);
1037
+ } else {
1038
+ printSuccess(`Runner appears live (last activity ${report.last_log_activity}).`);
1039
+ }
1040
+
1041
+ if (service.installed) {
1042
+ printInfo(` service: installed, ${service.enabled ? 'enabled' : 'NOT enabled'}, ${service.active ? 'active' : 'not active'}, linger ${service.linger ? 'ON' : 'OFF'}.`);
1043
+ if (!service.linger) {
1044
+ printWarning(' linger is OFF: this unit will not survive a reboot until an admin runs `loginctl enable-linger <user>` (or re-run `quox dev supervisor install`).');
1045
+ }
1046
+ } else {
1047
+ printInfo(` service: not installed. Run \`quox dev supervisor install\` for Option A persistence (survives reboot with no session open).`);
1048
+ }
1049
+
1050
+ if (heldBySelf) {
1051
+ printSuccess(` slot ${slotName}: held by this resident (present).`);
1052
+ } else if (foreignHolder) {
1053
+ printWarning(` slot ${slotName} is held by ${foreignHolder}: this supervisor is present but not holding it (it never seizes a hold another session owns).`);
1054
+ } else {
1055
+ printWarning(` slot ${slotName}: not held by this resident and not held by anyone else either (free). It should claim it on its next poll pass.`);
1056
+ }
1057
+
1058
+ if (report.last_turn_error) {
1059
+ printWarning(` last turn failed: ${report.last_turn_error}`);
1060
+ }
1061
+ });
1062
+ }