quoxcli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -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/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Dev commands — DEVSUITE-HUB Phase 2.
3
+ *
4
+ * `quox dev enable` is the CLI surface for the "register as dev server"
5
+ * trigger: constructs, signs, and delivers a quoxagent dev.CapabilityProfile
6
+ * onto a target fleet host via the collector's
7
+ * POST /api/internal/dev-profiles/enable (see
8
+ * services/collector/lib/devCapabilityProfileIssuer.js). Only
9
+ * `dev.register` is granted by default, matching what's real today in
10
+ * quoxagent's dev plugin family (internal/plugins/dev) — pass
11
+ * `--capabilities` to grant additional dev.* names.
12
+ */
13
+
14
+ import { formatDetail, formatOutput, printSuccess, printWarning, printInfo } from '../core/output.js';
15
+ import { shortLicenceLabel, matchesLicenceFilter } from './skillRightsFormat.js';
16
+ import { resolveOrgId } from './helpers.js';
17
+ import { resolveEndpoint } from '../api/endpoints.js';
18
+ import { QuoxApiError } from '../core/errors.js';
19
+ import { registerDevSupervisorCommands } from './devSupervisor.js';
20
+ import { registerDevResidentCommands } from './devResident.js';
21
+
22
+ export function registerDevCommands(program, ctx) {
23
+ const { client, auth, config } = ctx;
24
+
25
+ const dev = program
26
+ .command('dev')
27
+ .description('Manage dev-mode (DEVSUITE) fleet hosts');
28
+
29
+ dev
30
+ .command('enable')
31
+ .description('Register a fleet host as a dev server (signs + delivers a quoxagent capability profile)')
32
+ .argument('<hostId>', 'target host_id (matches the running quoxagent\'s agent.host_id)')
33
+ .option('--capabilities <list>', 'comma-separated dev.* capability names (default: dev.register)')
34
+ .option('--max-concurrent-sessions <n>', 'max concurrent dev.sessions tmux sessions', (v) => parseInt(v, 10))
35
+ .option('--ttl-seconds <n>', 'profile expiry, in seconds from now (default: no expiry)', (v) => parseInt(v, 10))
36
+ .option('--no-wait-pack', 'do not wait for the skill-pack install to be confirmed')
37
+ .action(async (hostId, options) => {
38
+ auth.requireAuth();
39
+ const orgId = resolveOrgId(auth, program);
40
+
41
+ // fleet #472: the collector's dev-enable admin gate evaluates the
42
+ // JWT's ACTIVE org, not the org named by --org, so a mismatched flag
43
+ // is silently mischecked and 403s even the target org's own owner.
44
+ // Until the server gate can check membership in the TARGET org,
45
+ // reject the mismatch loudly and name the working path.
46
+ const flagOrg = program.opts().org;
47
+ const sessionOrg = auth.getOrgId();
48
+ if (flagOrg && sessionOrg && flagOrg !== sessionOrg) {
49
+ throw new Error(
50
+ `--org ${flagOrg} differs from your active org context (${sessionOrg}). ` +
51
+ `The dev-enable admin check runs against the ACTIVE org, so this request would 403 ` +
52
+ `even for the target org's owner (fleet #472). ` +
53
+ `Run 'quox org switch ${flagOrg}' first, then re-run dev enable.`
54
+ );
55
+ }
56
+
57
+ const body = { host_id: hostId, org_id: orgId };
58
+ // quox#455: tell the provisioner which edge URL this working admin
59
+ // session actually uses, so the resident bootstrap lands with a real
60
+ // instance_url. A loopback hostname is fine to send: the collector
61
+ // swaps it for the host-reachable address server-side.
62
+ const enableBaseUrl = typeof config.getBaseUrl === 'function' ? config.getBaseUrl() : null;
63
+ if (enableBaseUrl) body.instance_url = enableBaseUrl;
64
+ if (options.capabilities) {
65
+ body.capabilities = options.capabilities.split(',').map((c) => c.trim()).filter(Boolean);
66
+ }
67
+ if (Number.isInteger(options.maxConcurrentSessions)) {
68
+ body.max_concurrent_sessions = options.maxConcurrentSessions;
69
+ }
70
+ if (Number.isInteger(options.ttlSeconds)) {
71
+ body.ttl_seconds = options.ttlSeconds;
72
+ }
73
+
74
+ const endpoint = resolveEndpoint('v1.dev.profiles.enable');
75
+ let data;
76
+ try {
77
+ data = await client.post(endpoint.path, { service: endpoint.service, body });
78
+ } catch (err) {
79
+ // quoxbastion#12 / quox#588: against a bastion in approval mode 'risky'
80
+ // the collector relays a "present a collector-signed
81
+ // X-Bastion-Approval-Grant" refusal, but there is no CLI path yet to
82
+ // mint/present that grant. Turn the dead-end 502 into an actionable
83
+ // failure rather than a raw relayed error the operator can't act on.
84
+ const detail = `${err?.message || ''} ${(() => { try { return JSON.stringify(err?.body || ''); } catch { return ''; } })()}`;
85
+ if (/X-Bastion-Approval-Grant|approval required/i.test(detail)) {
86
+ printWarning(`Bastion is in approval mode 'risky': dev enable needs a collector-signed approval grant, and the CLI cannot mint or present one yet.`);
87
+ printInfo(`Known gap, tracked as quoxai/quox#588 (and quoxbastion#12) — the HITL grant-mint + auto-retry path is not built.`);
88
+ printInfo(`Until it ships, a bastion/org admin can lower this bastion's approval mode below 'risky', then re-run:`);
89
+ printInfo(` quox dev enable ${hostId}`);
90
+ }
91
+ throw err;
92
+ }
93
+
94
+ formatDetail(data, { format: program.opts().output, title: `Dev profile delivered to ${hostId}` });
95
+ if (data && data.delivered) {
96
+ printSuccess(`${hostId} registered as a dev server (capabilities: ${(data.capabilities || []).join(', ')}).`);
97
+ }
98
+
99
+ // 2026-08-19: DELIVERED IS NOT LIVE. quoxagent loads the signed dev
100
+ // capability profile ONCE AT STARTUP (internal/agent/agent.go, see
101
+ // LoadDevCapabilityProfile) with no reload and no watch, so a profile
102
+ // that lands on disk is not adopted until the agent restarts. This
103
+ // command happily printed "registered as a dev server (capabilities:
104
+ // dev.register, dev.build, dev.sessions, dev.plan)" while the host went
105
+ // on reporting the previous three, on a heartbeat seconds old. The
106
+ // capability pill did not render and nothing said why.
107
+ //
108
+ // Exactly the bug the skill-pack block below was written to kill, one
109
+ // layer up: "success" meant the WRITE succeeded, never that the thing
110
+ // took effect. So do what that block does — go and look.
111
+ if (data && data.delivered && Array.isArray(data.capabilities) && data.capabilities.length) {
112
+ const signed = [...data.capabilities].sort();
113
+ let adopted = null;
114
+ try {
115
+ const listEp = resolveEndpoint('v1.dev.servers.list');
116
+ const deadline = Date.now() + 30000;
117
+ while (Date.now() < deadline) {
118
+ await new Promise((r) => setTimeout(r, 5000));
119
+ let list = null;
120
+ try {
121
+ // org_id is REQUIRED: the dev-servers handler fails closed rather
122
+ // than falling back to an unscoped list (fixed 2026-08-19, it used
123
+ // to list across orgs). Omitting it here got a refusal, which this
124
+ // block then reported as "registry unreadable".
125
+ list = await client.get(listEp.path, { service: listEp.service, query: { org_id: orgId } });
126
+ } catch { list = null; }
127
+ const rows = Array.isArray(list?.servers) ? list.servers : [];
128
+ const row = rows.find((h) => (h.host_id || h.hostId) === hostId);
129
+ const caps = row && (row.enabled_capabilities || row.enabledCapabilities);
130
+ const parsed = typeof caps === 'string' ? JSON.parse(caps) : caps;
131
+ if (Array.isArray(parsed)) {
132
+ adopted = [...parsed].sort();
133
+ if (signed.every((c) => adopted.includes(c))) break;
134
+ }
135
+ }
136
+ } catch { adopted = null; }
137
+
138
+ if (adopted === null) {
139
+ console.error(`Could not confirm ${hostId} adopted the new profile (registry unreadable). Verify with: quox dev doctor ${hostId}`);
140
+ } else {
141
+ const missing = signed.filter((c) => !adopted.includes(c));
142
+ if (missing.length) {
143
+ // Phase 7: the collector restarts the agent itself now (see
144
+ // agent_refresh in the response), so a lingering mismatch means
145
+ // either the restart was deferred for live sessions, or
146
+ // something on the host is genuinely wrong.
147
+ const refresh = data.agent_refresh || {};
148
+ if (refresh.deferred_live_sessions > 0) {
149
+ console.error(
150
+ `NOT LIVE YET: ${hostId} still reports [${adopted.join(', ')}]. The agent restart was deferred because ` +
151
+ `${refresh.deferred_live_sessions} live session(s) would die with it. The profile and binary are delivered; ` +
152
+ `the next agent restart adopts them. Force it when the sessions are done:\n` +
153
+ ` systemctl --user restart quoxagent (user service on dev VMs)\n` +
154
+ `Then confirm with: quox dev doctor ${hostId}`
155
+ );
156
+ } else {
157
+ // #462 (footgun 4, exampleestate-website 2026-08-22): the most
158
+ // common cause of delivered+restarted+still-empty is the agent
159
+ // being UNABLE TO VERIFY the profile: the signing-key fetch
160
+ // needs org/collector/service-key env in the unit. The
161
+ // collector's verifier-env leg stamps the two non-secret vars
162
+ // and reports whether the seam key file exists; name that
163
+ // diagnosis here instead of sending the operator to the
164
+ // journal blind.
165
+ let hint = `Check the agent journal on the host, then: quox dev doctor ${hostId}`;
166
+ if (refresh.seam_key === 'missing') {
167
+ hint = `DIAGNOSIS: the host has no seam service key file, so the agent cannot fetch the org signing key ` +
168
+ `and cannot verify the profile (profile_verified stays false). Provision the key file the env drop-in ` +
169
+ `points at (see quoxpert-dev-servers footgun 4), then restart the agent.`;
170
+ } else if (refresh.verifier_env === 'error') {
171
+ hint = `DIAGNOSIS: stamping the signing-key-fetch env onto the unit failed (${refresh.verifier_env_error || 'unknown'}). ` +
172
+ `Without QUOXAGENT_ORG_ID and QUOXAGENT_COLLECTOR_URL in the agent unit the profile cannot be verified.`;
173
+ }
174
+ console.error(
175
+ `NOT LIVE: ${hostId} still reports [${adopted.join(', ')}] and has not adopted [${missing.join(', ')}] ` +
176
+ `even though the enable ${refresh.restarted ? 'restarted the agent' : 'could not restart the agent'} ` +
177
+ `(agent_refresh: ${JSON.stringify(refresh)}). ${hint}`
178
+ );
179
+ }
180
+ } else {
181
+ printSuccess(`Host has adopted the profile: ${adopted.join(', ')}.`);
182
+ }
183
+ }
184
+ }
185
+
186
+ // DEVSUITE-SKILLS P6f: the pack install fires async behind enable, and
187
+ // "success" used to mean only the profile landed — the arg-limit bug
188
+ // shipped broken packs behind a green enable for exactly that reason.
189
+ // Poll the durable pack status until confirmed (or say plainly that it
190
+ // is not confirmed and name the follow-up verb).
191
+ if (data && data.delivered && options.waitPack !== false) {
192
+ const statusEp = resolveEndpoint('v1.dev.skills.status', { hostId });
193
+ const deadline = Date.now() + 90000;
194
+ process.stdout.write('Waiting for skill-pack install confirmation');
195
+ let pack = null;
196
+ while (Date.now() < deadline) {
197
+ await new Promise((r) => setTimeout(r, 5000));
198
+ process.stdout.write('.');
199
+ try {
200
+ pack = await client.get(statusEp.path, { service: statusEp.service });
201
+ } catch { pack = null; }
202
+ if (pack && pack.status && pack.status !== 'unknown') break;
203
+ }
204
+ process.stdout.write('\n');
205
+ if (pack && (pack.status === 'current' || pack.installedVersion)) {
206
+ printSuccess(`Skill pack v${pack.installedVersion || '?'} confirmed on ${hostId} (${pack.status}).`);
207
+ } else {
208
+ console.error(`Skill pack NOT confirmed on ${hostId} within 90s — the profile is live, the pack is not proven. Check with: quox dev skills status ${hostId} (or force it: quox dev skills sync ${hostId})`);
209
+ }
210
+ }
211
+ });
212
+
213
+ // QUOXPERTICUS P5: skill-pack parity (the pack routes existed with no verb).
214
+ const skills = dev
215
+ .command('skills')
216
+ .description('Dev skill-pack delivery (signed core dev pack)');
217
+
218
+ skills
219
+ .command('status')
220
+ .description('Show the installed pack version/hash on a dev host vs current, with SkillRights labels')
221
+ .argument('<hostId>', 'target host_id')
222
+ .option('--rights [licence]', 'list per-skill SkillRights declarations; optionally filter by open|notrain|reserved|undeclared or a full identifier')
223
+ .action(async (hostId, options) => {
224
+ auth.requireAuth();
225
+ const ep = resolveEndpoint('v1.dev.skills.status', { hostId });
226
+ const data = await client.get(ep.path, { service: ep.service });
227
+
228
+ // SKILLRIGHTS #478 phase 11 (pricing-free): rights are load-bearing
229
+ // data, so render them deliberately rather than dumping the raw
230
+ // object. Summary always; per-skill table behind --rights, with an
231
+ // optional licence filter (the CLI half of the marketplace's
232
+ // "licence filter").
233
+ const { rights, installedRights, ...core } = data;
234
+ formatDetail(core, { format: program.opts().output, title: `Skill pack status: ${hostId}` });
235
+ if (rights) {
236
+ const byLicence = Object.entries(rights.byLicense || {})
237
+ .map(([id, n]) => `${shortLicenceLabel(id)} x${n}`)
238
+ .join(', ');
239
+ printInfo(`Rights: ${rights.declared}/${rights.total} declared${byLicence ? ` (${byLicence})` : ''}${rights.undeclared ? `, ${rights.undeclared} undeclared` : ''}.`);
240
+ }
241
+ if (options.rights) {
242
+ const filter = typeof options.rights === 'string' ? options.rights : null;
243
+ const entries = (rights && rights.entries) || [];
244
+ const rows = entries
245
+ .filter((e) => matchesLicenceFilter(e.license, filter))
246
+ .map((e) => ({ skill: e.ref, licence: shortLicenceLabel(e.license) }));
247
+ formatOutput({
248
+ columns: ['skill', 'licence'],
249
+ headers: ['Skill', 'Licence'],
250
+ rows,
251
+ }, { format: program.opts().output, emptyMessage: filter ? `No skills matching licence filter '${filter}'.` : 'No rights entries.' });
252
+ if (installedRights && Array.isArray(installedRights)) {
253
+ const drifted = entries.length !== installedRights.length
254
+ || entries.some((e) => {
255
+ const got = installedRights.find((i) => i.ref === e.ref);
256
+ return !got || got.license !== e.license;
257
+ });
258
+ if (drifted) printWarning('Declared rights differ from what this host RECEIVED at last confirmed sync (installedRights); re-sync to update the host-side rights manifest.');
259
+ }
260
+ }
261
+ });
262
+
263
+ skills
264
+ .command('sync')
265
+ .description('Install or update the signed core dev pack on a dev host')
266
+ .argument('<hostId>', 'target host_id')
267
+ .action(async (hostId) => {
268
+ auth.requireAuth();
269
+ const ep = resolveEndpoint('v1.dev.skills.sync', { hostId });
270
+ let data;
271
+ try {
272
+ data = await client.post(ep.path, { service: ep.service, body: {} });
273
+ } catch (err) {
274
+ // SKILLRIGHTS #478 Phase 11: under the require-acceptance policy the
275
+ // collector refuses the download until a person has accepted the
276
+ // terms. Name the exact command rather than leaving a bare 409: an
277
+ // unactionable refusal is how a governance control becomes a thing
278
+ // people route around.
279
+ if (err && err.body && err.body.error === 'acceptance_required') {
280
+ printWarning(`Refused: nobody in this org has accepted the current terms for pack '${err.body.packId}'.`);
281
+ printInfo(`Accept them (as a user, not a service credential), then re-run the sync:`);
282
+ printInfo(` quox dev skills accept ${err.body.packId}`);
283
+ printInfo(` quox dev skills sync ${hostId}`);
284
+ }
285
+ throw err;
286
+ }
287
+ const { rightsNotice, ...detail } = data;
288
+ formatDetail(detail, { format: program.opts().output, title: `Skill pack sync: ${hostId}` });
289
+ // The distribution terms travel with every sync (phase 11 contract
290
+ // enforcement): show them, do not bury them in the JSON.
291
+ if (rightsNotice) printInfo(rightsNotice);
292
+ });
293
+
294
+ // SKILLRIGHTS #478 Phase 11 — AFFIRMATIVE ACCEPTANCE. The whole point of
295
+ // this command is that a human reads the terms and says yes: it PRINTS the
296
+ // terms in full, then requires an interactive confirmation or an explicit
297
+ // --i-accept. It never accepts silently, and a service credential is
298
+ // refused server-side (user_assent_required), so automation cannot stand in
299
+ // for a person here.
300
+ skills
301
+ .command('accept')
302
+ .description('Read the QuoxSkills distribution terms for a pack and record your acceptance of them')
303
+ .argument('<packId>', 'pack id (e.g. quox-core-dev-pack)')
304
+ .option('--i-accept', 'accept without the interactive prompt (still records YOU as the accepting user)')
305
+ .action(async (packId, options) => {
306
+ auth.requireAuth();
307
+
308
+ const offerEp = resolveEndpoint('skills.acceptance.offer', { packId });
309
+ const offer = await client.get(offerEp.path, { service: offerEp.service });
310
+
311
+ // The terms, in full. Not a link, not a summary: an acceptance of text
312
+ // the actor was never shown is not evidence of anything.
313
+ printInfo(`Terms for pack '${offer.packId}'${offer.packVersion ? ` (version ${offer.packVersion})` : ''}:`);
314
+ console.log('');
315
+ console.log(offer.termsBytes);
316
+ console.log('');
317
+
318
+ const rights = offer.rights || {};
319
+ const byLicence = Object.entries(rights.byLicense || {})
320
+ .map(([id, n]) => `${shortLicenceLabel(id)} x${n}`)
321
+ .join(', ');
322
+ printInfo(`Declared rights: ${rights.declared ?? 0}/${rights.total ?? 0} declared${byLicence ? ` (${byLicence})` : ''}${rights.undeclared ? `, ${rights.undeclared} undeclared` : ''}.`);
323
+ printInfo(`Pack content hash: ${offer.contentHash}`);
324
+ printInfo(`Declared rights hash: ${offer.rightsSha256}`);
325
+ printInfo(`Terms hash: ${offer.termsSha256} (presentation ${offer.presentationVersion})`);
326
+
327
+ if (!options.iAccept) {
328
+ const { default: inquirer } = await import('inquirer');
329
+ const ans = await inquirer.prompt([{
330
+ type: 'confirm',
331
+ name: 'confirm',
332
+ message: `Do you accept these terms, on behalf of your organisation, for exactly this pack content?`,
333
+ default: false,
334
+ }]);
335
+ if (!ans.confirm) {
336
+ printInfo('Not accepted. Nothing was recorded.');
337
+ return;
338
+ }
339
+ }
340
+
341
+ const recordEp = resolveEndpoint('skills.acceptance.record');
342
+ const result = await client.post(recordEp.path, {
343
+ service: recordEp.service,
344
+ body: {
345
+ challenge: offer.challenge,
346
+ termsSha256: offer.termsSha256,
347
+ contentHash: offer.contentHash,
348
+ rightsSha256: offer.rightsSha256,
349
+ },
350
+ });
351
+
352
+ printSuccess(`Acceptance recorded (id ${result.acceptanceId}).`);
353
+ // The honest claim, verbatim from the server, plus its limits. No
354
+ // surface may say more than this sentence says.
355
+ printInfo(result.honestClaim);
356
+ if (result.claimLimits) printInfo(result.claimLimits);
357
+ if (!result.envelopeId) {
358
+ printWarning('No evidence envelope was recorded for this acceptance (transcript storage disabled?). The acceptance row stands; the AEE reference does not.');
359
+ }
360
+ });
361
+
362
+ // SLOTS-TMUX step B: interactive tmux sessions on a dev host. quoxagent's
363
+ // five dev.sessions methods had NO caller (note #2877): no collector route,
364
+ // no quox verb. These are the verbs. Each one is a single short signed
365
+ // envelope delivered over Bastion; the session itself lives on the host's
366
+ // tmux server between calls, so it survives detach and outlives any one
367
+ // command here.
368
+ const sessions = dev
369
+ .command('sessions')
370
+ .description('Interactive tmux sessions on a dev host (survive detach; drive with send/pane)');
371
+
372
+ sessions
373
+ .command('list')
374
+ .description('List live tmux sessions on a dev host')
375
+ .argument('<hostId>', 'target host_id')
376
+ .action(async (hostId) => {
377
+ auth.requireAuth();
378
+ const ep = resolveEndpoint('v1.dev.sessions.list', { hostId });
379
+ const data = await client.get(ep.path, { service: ep.service });
380
+ formatDetail(data, { format: program.opts().output, title: `tmux sessions: ${hostId}` });
381
+ });
382
+
383
+ sessions
384
+ .command('new')
385
+ .description('Create a tmux session on a dev host')
386
+ .argument('<hostId>', 'target host_id')
387
+ .option('--name <name>', 'session name (default: agent-generated)')
388
+ .option('--backend <backend>', 'what runs in the pane: shell | claude | codex', 'shell')
389
+ .action(async (hostId, options) => {
390
+ auth.requireAuth();
391
+ const ep = resolveEndpoint('v1.dev.sessions.new', { hostId });
392
+ const body = { backend: options.backend };
393
+ if (options.name) body.name = options.name;
394
+ const data = await client.post(ep.path, { service: ep.service, body });
395
+ formatDetail(data, { format: program.opts().output, title: `tmux session created on ${hostId}` });
396
+ if (data && data.session_id) {
397
+ printSuccess(`Session ${data.session_id} (${data.name || 'unnamed'}) is live. Drive it with:`);
398
+ printInfo(` quox dev sessions send ${hostId} ${data.session_id} "<command>"`);
399
+ printInfo(` quox dev sessions pane ${hostId} ${data.session_id}`);
400
+ }
401
+ });
402
+
403
+ // quox#451: `new --name X` makes X the tmux session NAME, but send/pane/kill
404
+ // resolved only the agent's internal id, so reusing the name you just typed
405
+ // 422'd "session not found". On exactly that refusal, retry once by name.
406
+ // An explicit --by-name skips straight to the name lookup, as before.
407
+ async function withNameFallback(byName, call) {
408
+ try {
409
+ return await call(byName);
410
+ } catch (err) {
411
+ const said = [err && err.message, err && err.code, err && err.body && err.body.error].filter(Boolean).join(' ');
412
+ if (!byName && err instanceof QuoxApiError && err.status === 422 && /session not found/i.test(said)) {
413
+ return call(true);
414
+ }
415
+ throw err;
416
+ }
417
+ }
418
+
419
+ sessions
420
+ .command('send')
421
+ .description('Send one line of input to a session (Enter is appended)')
422
+ .argument('<hostId>', 'target host_id')
423
+ .argument('<sessionId>', 'session id from `new` or `list` (or tmux name with --by-name)')
424
+ .argument('<keys>', 'the line to type into the pane')
425
+ .option('--by-name', 'treat the identifier as the tmux session NAME (orphaned sessions have no id)')
426
+ .action(async (hostId, sessionId, keys, options) => {
427
+ auth.requireAuth();
428
+ const data = await withNameFallback(!!options.byName, (byName) => {
429
+ const ep = resolveEndpoint('v1.dev.sessions.keys', { hostId, sessionId, byName });
430
+ return client.post(ep.path, { service: ep.service, body: { keys } });
431
+ });
432
+ if (data && data.sent) printSuccess(`Sent to ${sessionId} on ${hostId}.`);
433
+ else formatDetail(data, { format: program.opts().output });
434
+ });
435
+
436
+ sessions
437
+ .command('pane')
438
+ .description('Read the current pane contents of a session')
439
+ .argument('<hostId>', 'target host_id')
440
+ .argument('<sessionId>', 'session id (or tmux session name with --by-name)')
441
+ .option('--by-name', 'treat the identifier as the tmux session NAME (orphaned sessions have no id)')
442
+ .action(async (hostId, sessionId, options) => {
443
+ auth.requireAuth();
444
+ const data = await withNameFallback(!!options.byName, (byName) => {
445
+ const ep = resolveEndpoint('v1.dev.sessions.pane', { hostId, sessionId, byName });
446
+ return client.get(ep.path, { service: ep.service });
447
+ });
448
+ if (program.opts().output === 'json') {
449
+ formatDetail(data, { format: 'json' });
450
+ } else {
451
+ // The pane IS the output: print it raw, trailing blank lines trimmed.
452
+ process.stdout.write(String(data && data.pane || '').replace(/\n+$/, '') + '\n');
453
+ }
454
+ });
455
+
456
+ sessions
457
+ .command('kill')
458
+ .description('Kill a tmux session on a dev host')
459
+ .argument('<hostId>', 'target host_id')
460
+ .argument('<sessionId>', 'session id (or tmux session name with --by-name)')
461
+ .option('--by-name', 'treat the identifier as the tmux session NAME (orphaned sessions have no id)')
462
+ .action(async (hostId, sessionId, options) => {
463
+ auth.requireAuth();
464
+ const data = await withNameFallback(!!options.byName, (byName) => {
465
+ const ep = resolveEndpoint('v1.dev.sessions.kill', { hostId, sessionId, byName });
466
+ return client.delete(ep.path, { service: ep.service });
467
+ });
468
+ if (data && data.killed) printSuccess(`Session ${sessionId} killed on ${hostId}.`);
469
+ else formatDetail(data, { format: program.opts().output });
470
+ });
471
+
472
+ // DEVSUITE phase 9: "a quox dev doctor verb (pack version, rules block, MCP
473
+ // health, session freshness) so 'is it even taking?' has a product answer".
474
+ //
475
+ // The question this exists to kill: an operator runs `quox dev enable`, gets
476
+ // a success line, and then has no way to tell whether the host is actually a
477
+ // working dev server. Every check below reads REAL registry state and reports
478
+ // what it found; nothing is inferred from the enable having returned 200,
479
+ // because a 200 from the enable is exactly the evidence that was already
480
+ // insufficient.
481
+ //
482
+ // Read-only by construction. A doctor that changes things cannot be run
483
+ // freely when you are unsure, which is precisely when you want to run it.
484
+ dev
485
+ .command('doctor')
486
+ .description('Check whether a host is really working as a dev server')
487
+ .argument('[hostId]', 'target host_id (default: every registered dev server)')
488
+ .action(async (hostId) => {
489
+ auth.requireAuth();
490
+ const orgId = resolveOrgId(auth, program);
491
+
492
+ const listEp = resolveEndpoint('v1.dev.servers.list');
493
+ const list = await client.get(listEp.path, { service: listEp.service, query: { org_id: orgId } });
494
+ const all = Array.isArray(list?.servers) ? list.servers : [];
495
+ const targets = hostId ? all.filter((s) => s.host_id === hostId) : all;
496
+
497
+ if (hostId && targets.length === 0) {
498
+ // Do not say "unhealthy": the host is not registered to this org at
499
+ // all, which is a different problem with a different fix.
500
+ printWarning(`${hostId} is not a registered dev server for this org.`);
501
+ printInfo(' Register it with `quox dev enable ' + hostId + '`.');
502
+ return;
503
+ }
504
+ if (targets.length === 0) {
505
+ printWarning('No dev servers are registered for this org.');
506
+ return;
507
+ }
508
+
509
+ const report = [];
510
+ for (const s of targets) {
511
+ const findings = [];
512
+
513
+ // 1. Did the signed capability profile verify? An unverified profile
514
+ // means the host is registered but not trusted to act.
515
+ if (!s.profile_verified) findings.push('profile not verified');
516
+
517
+ // 2. Is it still beating? A host that registered once and went quiet
518
+ // looks identical to a working one in a plain listing.
519
+ const ageMs = s.last_seen_at ? Date.now() - Number(s.last_seen_at) : null;
520
+ const staleMin = ageMs === null ? null : Math.round(ageMs / 60000);
521
+ if (staleMin === null) findings.push('never seen');
522
+ else if (staleMin > 15) findings.push(`last seen ${staleMin}m ago`);
523
+
524
+ // 3. Did anything actually get enabled? A profile with no capabilities
525
+ // is a dev server that can do nothing.
526
+ const enabled = Array.isArray(s.enabled_capabilities) ? s.enabled_capabilities : [];
527
+ if (enabled.length === 0) findings.push('no capabilities enabled');
528
+
529
+ // 3b. Did resident identity provisioning (quox#453 phase 15) land?
530
+ // A host with capabilities enabled but no resident identity can
531
+ // still deliver profiles, but nothing that needs the resident
532
+ // keypair (host-native signing) works. resident_state is only
533
+ // checked when the host has capabilities at all, matching #3's
534
+ // gate: an unenabled host has nothing resident identity is for
535
+ // yet.
536
+ if (enabled.length > 0 && s.resident_state !== 'provisioned') {
537
+ let finding = 'resident identity not provisioned (re-run quox dev enable)';
538
+ if (s.resident_state === 'failed' && s.resident_detail) {
539
+ finding += ` (${s.resident_detail})`;
540
+ }
541
+ findings.push(finding);
542
+ }
543
+
544
+ // 4. Is the skill pack on and current? This is the half of "enable"
545
+ // that silently does not happen, and the reason the verb exists.
546
+ let pack = null;
547
+ try {
548
+ const packEp = resolveEndpoint('v1.dev.skills.status', { hostId: s.host_id });
549
+ pack = await client.get(packEp.path, { service: packEp.service });
550
+ if (pack?.status === 'missing') findings.push('skill pack not installed');
551
+ else if (pack?.drifted) findings.push(`skill pack v${pack.installedVersion} behind v${pack.currentVersion}`);
552
+ } catch (err) {
553
+ // Unknown, not broken. Saying "pack missing" because a probe failed
554
+ // is the class of lie this whole verb is meant to prevent.
555
+ findings.push(`skill pack status unknown (${err.message})`);
556
+ }
557
+
558
+ // 5. Headless supervisor (quox#453 phase 15 follow-on, DEVSUITE-
559
+ // DEVMODE) — read-only status line, NOT a health finding. Most
560
+ // hosts legitimately run with the supervisor off; the switch is a
561
+ // human decision (server-side requireHumanAdmin() gate on the
562
+ // write endpoint, `quox dev supervisor` doctor's write-free
563
+ // sibling), not something this read-only verb should flag as a
564
+ // problem either way.
565
+ let supervisor = null;
566
+ try {
567
+ const supEp = resolveEndpoint('v1.dev.supervisor.get', { hostId: s.host_id });
568
+ const sup = await client.get(supEp.path, { service: supEp.service });
569
+ supervisor = { enabled: !!sup?.enabled, slot: sup?.slot || null };
570
+ } catch (err) {
571
+ // Unknown, not off. Same "don't invent a finding from a probe
572
+ // failure" posture as the skill-pack status check above.
573
+ supervisor = { enabled: null, slot: null, error: err.message };
574
+ }
575
+
576
+ report.push({
577
+ host_id: s.host_id,
578
+ healthy: findings.length === 0,
579
+ capabilities: enabled.length,
580
+ pack: pack ? (pack.status || 'unknown') : 'unknown',
581
+ last_seen_minutes: staleMin,
582
+ supervisor,
583
+ findings,
584
+ });
585
+ }
586
+
587
+ if (program.opts().output === 'json') {
588
+ formatDetail({ dev_servers: report }, { format: 'json' });
589
+ return;
590
+ }
591
+
592
+ for (const r of report) {
593
+ if (r.healthy) {
594
+ printSuccess(`${r.host_id}: ready (${r.capabilities} capabilities, pack ${r.pack})`);
595
+ } else {
596
+ printWarning(`${r.host_id}: ${r.findings.length} problem${r.findings.length === 1 ? '' : 's'}`);
597
+ for (const f of r.findings) printInfo(` - ${f}`);
598
+ }
599
+ const sup = r.supervisor;
600
+ if (sup && sup.error) {
601
+ printInfo(` supervisor: unknown (${sup.error})`);
602
+ } else if (sup && sup.enabled) {
603
+ printInfo(` supervisor: enabled${sup.slot ? ` (slot ${sup.slot})` : ''}`);
604
+ } else {
605
+ printInfo(' supervisor: disabled');
606
+ }
607
+ }
608
+ });
609
+
610
+ // DEVSUITE-DEVMODE (quox#453 phase 15 follow-on) — `quox dev supervisor`
611
+ // (see lib/commands/devSupervisor.js). Headless: a resident agent answers
612
+ // its slot channel with no interactive session open.
613
+ registerDevSupervisorCommands(dev, ctx);
614
+
615
+ // QUOXPERTICUS-REVISIT P3 — `quox dev resident` (see
616
+ // lib/commands/devResident.js). A FULL INTERACTIVE claude session as the
617
+ // resident, not the headless supervisor above: deploy/reboot/retire it
618
+ // through the governed tmux + slots planes, no ssh.
619
+ registerDevResidentCommands(dev, ctx);
620
+ }