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,239 @@
1
+ /**
2
+ * Output formatter for QuoxCLI
3
+ *
4
+ * All commands pass results through this module.
5
+ * Supports: table, json, csv, quiet output modes.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import Table from 'cli-table3';
10
+
11
+ /**
12
+ * Format and print tabular data
13
+ *
14
+ * @param {Object} result - { columns, headers, rows } or array of objects
15
+ * @param {Object} options - { format, title, emptyMessage }
16
+ */
17
+ export function formatOutput(result, options = {}) {
18
+ const format = options.format || getOutputFormat();
19
+ const rows = result.rows || result;
20
+ const columns = result.columns || (rows.length > 0 ? Object.keys(rows[0]) : []);
21
+ const headers = result.headers || columns;
22
+
23
+ // options.raw: the untransformed API records behind the display rows.
24
+ // --json exists for scripting, and scripts want DATA keys (name, state,
25
+ // holder_session), not the humanised table cells (NAME, HOLDER, "2d") the
26
+ // display rows carry -- `quox --json slot list` emitting NAME/STATE bit a
27
+ // live harness on 2026-08-20. Table/csv/quiet still render the display
28
+ // rows; json prefers raw when the caller provides it.
29
+ if (!Array.isArray(rows) || rows.length === 0) {
30
+ if (format === 'json') {
31
+ console.log(JSON.stringify(Array.isArray(options.raw) ? options.raw : []));
32
+ } else if (format === 'csv') {
33
+ console.log(headers.join(','));
34
+ } else if (format !== 'quiet') {
35
+ console.log(chalk.dim(options.emptyMessage || 'No results.'));
36
+ }
37
+ return;
38
+ }
39
+
40
+ switch (format) {
41
+ case 'json':
42
+ console.log(JSON.stringify(Array.isArray(options.raw) ? options.raw : rows, null, 2));
43
+ break;
44
+ case 'csv':
45
+ formatCsv(rows, columns, headers);
46
+ break;
47
+ case 'quiet':
48
+ formatQuiet(rows, columns);
49
+ break;
50
+ default:
51
+ formatTable(rows, columns, headers, options);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Format and print a single object (detail view)
57
+ */
58
+ export function formatDetail(obj, options = {}) {
59
+ const format = options.format || getOutputFormat();
60
+
61
+ if (format === 'json') {
62
+ console.log(JSON.stringify(obj, null, 2));
63
+ return;
64
+ }
65
+
66
+ if (format === 'quiet') {
67
+ console.log(obj.id || obj.name || '');
68
+ return;
69
+ }
70
+
71
+ // Key-value display
72
+ const fields = options.fields || Object.keys(obj);
73
+ const labels = options.labels || {};
74
+
75
+ if (options.title) {
76
+ console.log(chalk.bold(options.title));
77
+ console.log(chalk.dim('─'.repeat(Math.min(options.title.length + 10, 60))));
78
+ }
79
+
80
+ const maxLabel = Math.max(...fields.map(f => (labels[f] || f).length));
81
+
82
+ for (const field of fields) {
83
+ const label = (labels[field] || field).padEnd(maxLabel + 2);
84
+ const raw = obj[field];
85
+ if (raw !== null && typeof raw === 'object') {
86
+ console.log(` ${chalk.dim(label)}${formatNestedValue(raw)}`);
87
+ } else {
88
+ const value = formatValue(raw);
89
+ console.log(` ${chalk.dim(label)} ${value}`);
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Recursively render a nested object/array value for formatDetail, instead
96
+ * of falling through to formatValue's String(value) -> "[object Object]".
97
+ * Kept local to formatDetail (not folded into formatValue) so formatTable /
98
+ * formatCsv output is unchanged.
99
+ */
100
+ function formatNestedValue(value, indent = 4) {
101
+ if (value == null) return ` ${chalk.dim('-')}`;
102
+
103
+ if (Array.isArray(value)) {
104
+ if (value.length === 0) return ` ${chalk.dim('[]')}`;
105
+ const allPrimitive = value.every(v => v === null || typeof v !== 'object');
106
+ if (allPrimitive) return ` ${value.map(v => formatValue(v)).join(', ')}`;
107
+ return '\n' + value
108
+ .map(v => `${' '.repeat(indent)}- ${typeof v === 'object' && v !== null ? formatNestedValue(v, indent + 2).trimStart() : formatValue(v)}`)
109
+ .join('\n');
110
+ }
111
+
112
+ const keys = Object.keys(value);
113
+ if (keys.length === 0) return ` ${chalk.dim('{}')}`;
114
+ return '\n' + keys
115
+ .map(k => {
116
+ const v = value[k];
117
+ if (v !== null && typeof v === 'object') {
118
+ return `${' '.repeat(indent)}${chalk.dim(k + ':')}${formatNestedValue(v, indent + 2)}`;
119
+ }
120
+ return `${' '.repeat(indent)}${chalk.dim(k + ':')} ${formatValue(v)}`;
121
+ })
122
+ .join('\n');
123
+ }
124
+
125
+ /**
126
+ * Print a success message
127
+ */
128
+ export function printSuccess(message) {
129
+ const format = getOutputFormat();
130
+ if (format === 'json') {
131
+ console.log(JSON.stringify({ success: true, message }));
132
+ } else if (format !== 'quiet') {
133
+ console.log(chalk.green(message));
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Print a warning message
139
+ */
140
+ export function printWarning(message) {
141
+ if (getOutputFormat() !== 'quiet') {
142
+ console.error(chalk.yellow(message));
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Print info message (suppressed in quiet/json mode)
148
+ */
149
+ export function printInfo(message) {
150
+ const format = getOutputFormat();
151
+ if (format !== 'quiet' && format !== 'json') {
152
+ console.log(chalk.dim(message));
153
+ }
154
+ }
155
+
156
+ // -- Internal helpers --
157
+
158
+ function formatTable(rows, columns, headers, options) {
159
+ const table = new Table({
160
+ head: headers.map(h => chalk.bold(h)),
161
+ style: { head: [], border: [] },
162
+ chars: {
163
+ 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
164
+ 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
165
+ 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '',
166
+ 'right': '', 'right-mid': '', 'middle': ' ',
167
+ },
168
+ });
169
+
170
+ for (const row of rows) {
171
+ table.push(columns.map(col => formatValue(row[col])));
172
+ }
173
+
174
+ console.log(table.toString());
175
+
176
+ // Show count
177
+ if (rows.length > 10) {
178
+ console.log(chalk.dim(`\n${rows.length} items`));
179
+ }
180
+ }
181
+
182
+ function formatCsv(rows, columns, headers) {
183
+ console.log(headers.map(csvEscape).join(','));
184
+ for (const row of rows) {
185
+ console.log(columns.map(col => csvEscape(row[col])).join(','));
186
+ }
187
+ }
188
+
189
+ function formatQuiet(rows, columns) {
190
+ // Print first column (usually ID) of each row
191
+ const idCol = columns[0];
192
+ for (const row of rows) {
193
+ console.log(row[idCol] ?? '');
194
+ }
195
+ }
196
+
197
+ function csvEscape(value) {
198
+ const str = String(value ?? '');
199
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
200
+ return `"${str.replace(/"/g, '""')}"`;
201
+ }
202
+ return str;
203
+ }
204
+
205
+ function formatValue(value) {
206
+ if (value == null) return chalk.dim('-');
207
+ if (typeof value === 'boolean') return value ? chalk.green('yes') : chalk.dim('no');
208
+
209
+ const str = String(value);
210
+
211
+ // Color status values
212
+ const lower = str.toLowerCase();
213
+ if (['active', 'healthy', 'operational', 'approved', 'online', 'success'].includes(lower)) {
214
+ return chalk.green(str);
215
+ }
216
+ if (['degraded', 'warning', 'pending', 'in_progress', 'draft', 'stale'].includes(lower)) {
217
+ return chalk.yellow(str);
218
+ }
219
+ if (['error', 'down', 'failed', 'rejected', 'retired', 'offline', 'suspended', 'dead'].includes(lower)) {
220
+ return chalk.red(str);
221
+ }
222
+
223
+ // Truncate long values in table mode
224
+ if (str.length > 60) return str.slice(0, 57) + '...';
225
+
226
+ return str;
227
+ }
228
+
229
+ export function getOutputFormat() {
230
+ if (process.argv.includes('--json')) return 'json';
231
+ if (process.argv.includes('-q') || process.argv.includes('--quiet')) return 'quiet';
232
+
233
+ const idx = process.argv.indexOf('-o');
234
+ if (idx !== -1 && process.argv[idx + 1]) return process.argv[idx + 1];
235
+ const idx2 = process.argv.indexOf('--output');
236
+ if (idx2 !== -1 && process.argv[idx2 + 1]) return process.argv[idx2 + 1];
237
+
238
+ return 'table';
239
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Imported FIRST by the shipped bins (bin/quox.js, bin/quoxon.js), before anything that loads React: ES modules
3
+ * evaluate in import order, and React picks its build from NODE_ENV when it is first loaded. The development
4
+ * build roughly doubles CPU per animation frame (measured: ~51% vs 12-30% at 12 fps, 100x40). An explicit
5
+ * NODE_ENV (a developer's, or a test runner's) is always kept.
6
+ */
7
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';
@@ -0,0 +1,158 @@
1
+ /**
2
+ * @quox/glyph — the canonical JS implementation of the QuoxGlyph terminal
3
+ * output language (DEVSUITE-GLYPH). Spec: quox-skills
4
+ * quoxify/references/quoxglyph-spec.md. This package IS the spec's constants:
5
+ * ports must consume it (or test against it), never re-type the values.
6
+ *
7
+ * Grammar: <icon> NAME │ <verb object> │ <result> · <duration> [status]
8
+ * Colour: truecolor, degrades to plain when !isTTY or NO_COLOR.
9
+ * Brand gradient #38bdf8 -> #a855f7; rainbow RESERVED for completed.
10
+ */
11
+
12
+ export const GRAD = [[56, 189, 248], [168, 85, 247]];
13
+ export const RAINBOW = [
14
+ [255, 59, 48], [255, 149, 0], [255, 204, 0], [52, 199, 89],
15
+ [0, 199, 190], [10, 132, 255], [94, 92, 230], [191, 90, 242]
16
+ ];
17
+ export const STATUS = {
18
+ ok: { glyph: '✔', rgb: [48, 209, 88] },
19
+ fail: { glyph: '✖', rgb: [255, 69, 58] },
20
+ busy: { glyph: '◐', rgb: [56, 189, 248] },
21
+ warn: { glyph: '⚠', rgb: [255, 214, 10] },
22
+ block: { glyph: '⛔', rgb: null }
23
+ };
24
+ export const ICONS = {
25
+ repobrain: '🧠 REPOBRAIN', clarify: '🔍 CLARIFY', qlarity: '🔍 QLARITY',
26
+ quoxplan: '🧭 QUOXPLAN', doneengine: '🏁 DONEENGINE', quoxbuild: '🔨 QUOXBUILD',
27
+ quoxsignal: '📡 QUOXSIGNAL', bastion: '🛰️ BASTION', precompact: '🧿 PRECOMPACT',
28
+ brain2: '🌌 QORTEX',
29
+ watchtower: '📨 WATCHTOWER', metron: '⏳ METRON',
30
+ quoxseo: '📈 QUOXSEO', reach: '💥 REACH', vulcan: '🌋 VULCAN',
31
+ quoxvoice: '📞 QUOXVOICE', quoxsecurity: '🛡️ QUOXSECURITY', quoxpert: '🎓 QUOXPERT',
32
+ quoxslots: '📍 QUOXSLOTS',
33
+ quox: 'Ⓠ QUOX'
34
+ };
35
+ export const BUDGETS = { VERB_MAX: 48, RESULT_MAX: 40, LINE_MAX: 120, TREE_MAX: 6, TABLE_MAX: 6 };
36
+ export const PUA_Q = ''; // real horned Q, served by QuoxSymbols font
37
+
38
+ const R = '\x1b[0m', DIM = '\x1b[2m', BOLD = '\x1b[1m';
39
+ const fg = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
40
+
41
+ let forced = null;
42
+ /** Force colour on/off (null = auto: isTTY && !NO_COLOR). */
43
+ export function setColor(v) { forced = v; }
44
+ function on() {
45
+ if (forced !== null) return forced;
46
+ const env = process.env.QUOX_COLOR;
47
+ if (env === '0') return false;
48
+ if (env === '1' || env === '2') return true;
49
+ return Boolean(process.stdout && process.stdout.isTTY) && !process.env.NO_COLOR;
50
+ }
51
+
52
+ function iconFor(surface) {
53
+ let name = ICONS[surface] || `Ⓠ ${String(surface).toUpperCase()}`;
54
+ if (process.env.QUOX_BRAND_FONT === '1') name = name.replace('Ⓠ', PUA_Q);
55
+ return name;
56
+ }
57
+
58
+ /** "<icon> NAME" with NAME painted in the brand gradient, bold, so a Quox
59
+ * line stands out in tool output (owner 2026-10-05). Plain when colour is off. */
60
+ export function brandName(label, col = on()) {
61
+ if (!col) return label;
62
+ const i = label.indexOf(' ');
63
+ const icon = i > 0 ? label.slice(0, i) : '';
64
+ const chars = [...(i > 0 ? label.slice(i + 1) : label)];
65
+ const last = Math.max(chars.length - 1, 1);
66
+ const painted = chars.map((c, k) => fg(GRAD[0].map((v, j) => Math.round(v + (GRAD[1][j] - v) * k / last))) + c).join('');
67
+ return `${icon ? icon + ' ' : ''}${BOLD}${painted}${R}`;
68
+ }
69
+
70
+ const trunc = (s, max) => ([...s].length > max ? [...s].slice(0, max - 1).join('') + '…' : s);
71
+
72
+ /** The grammar event line. opts: {result, ms, status} */
73
+ export function line(surface, verb, opts = {}) {
74
+ const col = on();
75
+ const name = brandName(iconFor(surface), col);
76
+ const sep = col ? `${DIM}│${R}` : '│';
77
+ let tail = opts.result ? trunc(opts.result, BUDGETS.RESULT_MAX) : '';
78
+ if (opts.ms !== undefined) {
79
+ const d = col ? `${DIM}${opts.ms}ms${R}` : `${opts.ms}ms`;
80
+ tail = tail ? `${tail} ${col ? DIM + '·' + R : '·'} ${d}` : d;
81
+ }
82
+ const s = opts.status && STATUS[opts.status];
83
+ if (s) tail += ' ' + (col && s.rgb ? `${fg(s.rgb)}${s.glyph}${R}` : s.glyph);
84
+ const v = trunc(verb, BUDGETS.VERB_MAX);
85
+ return tail ? `${name} ${sep} ${v} ${sep} ${tail}` : `${name} ${sep} ${v}`;
86
+ }
87
+
88
+ /** Gradient progress meter: ▰▰▰▱ done/total (or pct when total omitted). */
89
+ export function meter(done, total, width = 8) {
90
+ const col = on();
91
+ const pct = total === undefined || total === null;
92
+ const ratio = pct ? Math.min(100, Math.max(0, done)) / 100 : (total > 0 ? done / total : 0);
93
+ const filled = Math.round(ratio * width);
94
+ const last = Math.max(1, width - 1);
95
+ let bar = '';
96
+ for (let i = 0; i < filled; i++) {
97
+ if (col) {
98
+ const t = i / last;
99
+ bar += fg(GRAD[0].map((v, k) => Math.round(v + (GRAD[1][k] - v) * t)));
100
+ }
101
+ bar += '▰';
102
+ }
103
+ if (col && filled) bar += R;
104
+ bar += (col ? DIM : '') + '▱'.repeat(Math.max(0, width - filled)) + (col ? R : '');
105
+ return pct ? `${bar} ${done}%` : `${bar} ${done}/${total}`;
106
+ }
107
+
108
+ /** Rainbow text. RESERVED for the `completed` lifecycle event. */
109
+ export function celebrate(text) {
110
+ if (!on()) return text;
111
+ const chars = [...text];
112
+ const last = Math.max(1, chars.length - 1);
113
+ return chars.map((ch, i) => {
114
+ const t = (i / last) * (RAINBOW.length - 1);
115
+ const seg = Math.min(RAINBOW.length - 2, Math.floor(t));
116
+ const local = t - seg;
117
+ return fg(RAINBOW[seg].map((v, k) => Math.round(v + (RAINBOW[seg + 1][k] - v) * local))) + ch;
118
+ }).join('') + R;
119
+ }
120
+
121
+ /** WARD receipt footer. */
122
+ export function receipt(hash, ts) {
123
+ const col = on();
124
+ const chain = col ? `${fg([168, 85, 247])}⛓${R}` : '⛓';
125
+ const label = col ? `${DIM}ward:${R}` : 'ward:';
126
+ const t = ts ? (col ? ` ${DIM}· witnessed ${ts}${R}` : ` · witnessed ${ts}`) : '';
127
+ return `${chain} ${label}${String(hash).slice(0, 12)}${t}`;
128
+ }
129
+
130
+ /**
131
+ * Lifecycle events (spec section 4b): the contract every product emits.
132
+ * Each returns a rendered line; the caller prints it.
133
+ */
134
+ export const events = {
135
+ created: (surface, thing, id) => line(surface, `${thing} created`, { result: id, status: 'ok' }),
136
+ updated: (surface, thing, id, delta) =>
137
+ line(surface, `${thing} updated`, { result: delta ? `${id} ${delta}` : id, status: 'ok' }),
138
+ progressed: (surface, step, id, done, total) =>
139
+ line(surface, step, { result: `${id} ${meter(done, total)}` }),
140
+ completed: (surface, thing, text) => line(surface, thing, { result: celebrate(text) }),
141
+ assigned: (surface, thing, holder) => line(surface, `${thing} assigned`, { result: holder, status: 'busy' }),
142
+ released: (surface, thing, id) => line(surface, `${thing} released`, { result: id, status: 'ok' }),
143
+ failed: (surface, thing, why) => line(surface, `${thing} failed`, { result: why, status: 'fail' }),
144
+ warned: (surface, thing, risk) => line(surface, thing, { result: risk, status: 'warn' })
145
+ };
146
+
147
+ // ---- quoxcli integration (not part of the @quox/glyph copy above) ----
148
+ // VENDORED from quox-plugin-sdk packages/quox-glyph/index.js until the
149
+ // owner-gated npm publish lands; the lockstep test in quoxglyph.test.js
150
+ // fails if this copy drifts from the package.
151
+ import { getOutputFormat } from './output.js';
152
+
153
+ /** Print a rendered glyph line, respecting --output json/quiet. */
154
+ export function printEvent(rendered) {
155
+ const format = getOutputFormat();
156
+ if (format === 'json' || format === 'quiet') return;
157
+ console.log(` ${rendered}`);
158
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Resident alarm — QUOXPERTICUS-REVISIT P5 "warden watchdog + loud alarm".
3
+ *
4
+ * While the warden pumps (residentPump.js) instead of answering, it also
5
+ * watches the resident it wards: is the tmux pane still there, has a
6
+ * permission dialog been stuck too long to ever clear itself, has the
7
+ * provider started refusing turns. Three independent conditions, each with
8
+ * its own escalating alert schedule (10m, 30m, then hourly) so a wedged
9
+ * resident is loud rather than silently absent, and each condition clears
10
+ * itself the moment its detector stops firing (no human-ack mechanism yet:
11
+ * clearing = the underlying thing is gone).
12
+ *
13
+ * Every I/O dependency (tmux session list, pane read, release/redeploy,
14
+ * the alert transport, the turn-receipt store, the clock) is injected, same
15
+ * contract as supervisorEngine.js and residentPump.js, so this runs under
16
+ * vitest with no real tmux host and no real Telegram bridge.
17
+ */
18
+
19
+ import { ERROR_NOTICE_WINDOW_MS } from './supervisorEngine.js';
20
+
21
+ // Gaps BETWEEN successive alerts for the SAME still-active condition:
22
+ // immediate on first detection (see shouldSendAlarm), then +10m, then +30m,
23
+ // then hourly forever until the condition clears.
24
+ export const ALARM_SCHEDULE_MS = [10 * 60 * 1000, 30 * 60 * 1000, ERROR_NOTICE_WINDOW_MS];
25
+
26
+ // A stuck permission dialog only becomes an alarm condition once it has sat
27
+ // unresolved this long. Below this age it is exactly what residentPump.js
28
+ // already logs: held input, not yet an incident.
29
+ export const STUCK_DIALOG_ALARM_THRESHOLD_MS = 10 * 60 * 1000;
30
+
31
+ // Pane-text scan patterns for a provider/credits failure. Deliberately
32
+ // simple substring/regex matches (this is pane TEXT, not a structured
33
+ // provider error object the way detectTurnError in supervisorEngine.js
34
+ // parses a headless turn's JSON output) — same spirit, different signal.
35
+ export const PROVIDER_ISSUE_PATTERNS = [
36
+ { pattern: 'out of usage credits', regex: /out of usage credits/i },
37
+ { pattern: 'usage limit', regex: /usage limit/i },
38
+ { pattern: 'provider error', regex: /provider error/i },
39
+ ];
40
+
41
+ export function scanPaneForProviderIssue(paneText) {
42
+ const text = String(paneText || '');
43
+ for (const { pattern, regex } of PROVIDER_ISSUE_PATTERNS) {
44
+ if (regex.test(text)) return { matched: true, pattern };
45
+ }
46
+ return { matched: false, pattern: null };
47
+ }
48
+
49
+ export function createInitialAlarmCondition() {
50
+ return { active: false, first_detected_at: null, last_alerted_at: null, alert_count: 0 };
51
+ }
52
+
53
+ export function createInitialAlarmState() {
54
+ return {
55
+ pane_dead_attempted: false,
56
+ pane_dead_redeploy_failed: false,
57
+ conditions: {
58
+ pane_dead: createInitialAlarmCondition(),
59
+ stuck_dialog: createInitialAlarmCondition(),
60
+ provider_error: createInitialAlarmCondition(),
61
+ },
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Advance one condition's state from a single boolean detection this pass.
67
+ * A detection while already active is a no-op (first_detected_at sticks to
68
+ * the original onset). No detection while active is a full reset: clearing
69
+ * a condition means the next detection is treated as a fresh onset, with
70
+ * its own fresh alert schedule.
71
+ */
72
+ export function updateAlarmCondition(conditionState, detected, now) {
73
+ const current = conditionState || createInitialAlarmCondition();
74
+ if (detected) {
75
+ if (current.active) return current;
76
+ return { ...current, active: true, first_detected_at: now };
77
+ }
78
+ if (current.active) return createInitialAlarmCondition();
79
+ return current;
80
+ }
81
+
82
+ /**
83
+ * Whether condition `conditionState` should alert right now. The first
84
+ * alert for a freshly-active condition fires immediately (no delay: a dead
85
+ * pane or a provider outage is news the moment it is confirmed). Every
86
+ * alert after that waits at least the schedule's gap for however many
87
+ * alerts have already fired, capped at the schedule's last (hourly) entry.
88
+ */
89
+ export function shouldSendAlarm(conditionState, now, schedule = ALARM_SCHEDULE_MS) {
90
+ if (!conditionState || !conditionState.active) return false;
91
+ if (conditionState.last_alerted_at == null) return true;
92
+ const idx = Math.min(Math.max((conditionState.alert_count || 0) - 1, 0), schedule.length - 1);
93
+ const gap = schedule[idx];
94
+ return (now - conditionState.last_alerted_at) >= gap;
95
+ }
96
+
97
+ export function recordAlarmSent(conditionState, now) {
98
+ return { ...conditionState, last_alerted_at: now, alert_count: (conditionState.alert_count || 0) + 1 };
99
+ }
100
+
101
+ /**
102
+ * Send one alarm through both transports in order, per spec: tg-send.sh
103
+ * first (the owner's phone), `quox slot say` second as the fallback when
104
+ * tg-send.sh is unavailable or fails — never silent, and the absence of the
105
+ * script is itself treated as "alarm-transport-unavailable", not a reason
106
+ * to skip the slot notice.
107
+ */
108
+ export async function sendAlarm({ tgSend, postSlotNotice, text, sessionName = 'WARDEN', log = () => {} }) {
109
+ let tgSent = false;
110
+ if (tgSend) {
111
+ try {
112
+ await tgSend(sessionName, text);
113
+ tgSent = true;
114
+ } catch (err) {
115
+ log('warn', `alarm: tg-send.sh failed, falling back to slot say: ${err.message}`);
116
+ }
117
+ } else {
118
+ log('warn', 'alarm: alarm-transport-unavailable (tg-send.sh not found), falling back to slot say');
119
+ }
120
+ if (!tgSent) {
121
+ try {
122
+ await postSlotNotice(text);
123
+ } catch (err) {
124
+ log('error', `alarm: slot say fallback also failed: ${err.message}`);
125
+ }
126
+ }
127
+ return { tg_sent: tgSent };
128
+ }
129
+
130
+ /**
131
+ * Run one warden watchdog pass.
132
+ *
133
+ * Injected dependencies:
134
+ * listSessions() -> array of the host's tmux sessions ({name, ...})
135
+ * readPane() -> current pane text (only called while the pane
136
+ * is confirmed alive)
137
+ * releaseSlot() -> release the resident's slot (best-effort)
138
+ * redeploy() -> run the redeploy path (reuses `quox dev
139
+ * resident deploy`; throws on failure)
140
+ * claimFallback() -> attempt to claim the slot as this warden's
141
+ * own fallback voice (only called when redeploy
142
+ * fails); returns {claimed: bool}
143
+ * postSlotNotice(text) -> slot `say`
144
+ * recordTurn(fields) -> turn receipt (outcome/error_note only;
145
+ * NEVER a new outcome value — see
146
+ * devSupervisorTurnsStore.js OUTCOMES)
147
+ * tgSend(session, text) -> shells out to tg-send.sh; absent/failing is
148
+ * handled by sendAlarm above, never thrown here
149
+ * stuckDialogSinceMs -> residentPump.js's state.stuck_dialog_since
150
+ * for this slot (single source of truth for
151
+ * dialog detection; this module only ages it)
152
+ */
153
+ export async function runWardenPass({
154
+ hostId,
155
+ slot,
156
+ sessionName,
157
+ listSessions,
158
+ readPane,
159
+ releaseSlot,
160
+ redeploy,
161
+ claimFallback,
162
+ postSlotNotice,
163
+ recordTurn,
164
+ tgSend,
165
+ stuckDialogSinceMs = null,
166
+ state,
167
+ log = () => {},
168
+ now = () => Date.now(),
169
+ schedule = ALARM_SCHEDULE_MS,
170
+ stuckDialogThresholdMs = STUCK_DIALOG_ALARM_THRESHOLD_MS,
171
+ }) {
172
+ const base = state || createInitialAlarmState();
173
+ const nextState = {
174
+ pane_dead_attempted: base.pane_dead_attempted ?? false,
175
+ pane_dead_redeploy_failed: base.pane_dead_redeploy_failed ?? false,
176
+ conditions: {
177
+ pane_dead: base.conditions?.pane_dead || createInitialAlarmCondition(),
178
+ stuck_dialog: base.conditions?.stuck_dialog || createInitialAlarmCondition(),
179
+ provider_error: base.conditions?.provider_error || createInitialAlarmCondition(),
180
+ },
181
+ };
182
+
183
+ const nowMs = now();
184
+ const report = { pane_alive: null, redeployed: false, redeploy_error: null, fallback_claimed: false, alerts_sent: [] };
185
+
186
+ const sessions = await listSessions();
187
+ const paneAlive = !!(sessions || []).find((s) => s.name === sessionName);
188
+ report.pane_alive = paneAlive;
189
+
190
+ let paneText = null;
191
+
192
+ if (!paneAlive) {
193
+ if (!nextState.pane_dead_attempted) {
194
+ nextState.pane_dead_attempted = true;
195
+ log('warn', `warden: resident session ${sessionName} on ${hostId} is not present (pane dead); releasing the slot and redeploying.`);
196
+ try {
197
+ await releaseSlot();
198
+ } catch (err) {
199
+ log('warn', `warden: releasing ${slot} failed (continuing): ${err.message}`);
200
+ }
201
+ try {
202
+ await redeploy();
203
+ report.redeployed = true;
204
+ nextState.pane_dead_redeploy_failed = false;
205
+ nextState.conditions.pane_dead = createInitialAlarmCondition();
206
+ try {
207
+ await postSlotNotice(`Resident on ${hostId} lost its pane; redeployed automatically.`);
208
+ } catch (err) {
209
+ log('warn', `warden: redeploy notice failed to post: ${err.message}`);
210
+ }
211
+ try {
212
+ await recordTurn({ outcome: 'error', error_note: 'pane_dead_auto_redeployed' });
213
+ } catch (err) {
214
+ log('warn', `warden: redeploy receipt failed to post: ${err.message}`);
215
+ }
216
+ } catch (err) {
217
+ report.redeploy_error = err.message;
218
+ nextState.pane_dead_redeploy_failed = true;
219
+ log('error', `warden: redeploy failed for ${hostId}: ${err.message}`);
220
+ try {
221
+ await postSlotNotice(`Resident on ${hostId} lost its pane and automatic redeploy FAILED (${err.message}). A human should check this host.`);
222
+ } catch (err2) {
223
+ log('warn', `warden: failure notice failed to post: ${err2.message}`);
224
+ }
225
+ try {
226
+ const fb = await claimFallback();
227
+ report.fallback_claimed = !!(fb && fb.claimed);
228
+ } catch (err2) {
229
+ log('warn', `warden: fallback claim attempt failed: ${err2.message}`);
230
+ }
231
+ }
232
+ }
233
+ if (nextState.pane_dead_redeploy_failed) {
234
+ nextState.conditions.pane_dead = updateAlarmCondition(nextState.conditions.pane_dead, true, nowMs);
235
+ }
236
+ } else {
237
+ if (nextState.pane_dead_attempted || nextState.conditions.pane_dead.active) {
238
+ log('info', `warden: resident session ${sessionName} on ${hostId} is present again; pane-dead condition cleared.`);
239
+ }
240
+ nextState.pane_dead_attempted = false;
241
+ nextState.pane_dead_redeploy_failed = false;
242
+ nextState.conditions.pane_dead = updateAlarmCondition(nextState.conditions.pane_dead, false, nowMs);
243
+
244
+ try {
245
+ paneText = await readPane();
246
+ } catch (err) {
247
+ log('warn', `warden: pane read failed on ${hostId} (skipping provider-error scan this pass): ${err.message}`);
248
+ }
249
+ }
250
+
251
+ // Stuck dialog: this module never detects the dialog itself, it only ages
252
+ // what residentPump.js already found (single source of truth).
253
+ const dialogStuck = stuckDialogSinceMs != null && (nowMs - stuckDialogSinceMs) >= stuckDialogThresholdMs;
254
+ nextState.conditions.stuck_dialog = updateAlarmCondition(nextState.conditions.stuck_dialog, dialogStuck, nowMs);
255
+
256
+ if (paneText != null) {
257
+ const scan = scanPaneForProviderIssue(paneText);
258
+ nextState.conditions.provider_error = updateAlarmCondition(nextState.conditions.provider_error, scan.matched, nowMs);
259
+ }
260
+
261
+ const conditionAlerts = [
262
+ ['pane_dead', `Resident on ${hostId}: pane is dead and automatic redeploy failed. A human should check this host.`],
263
+ ['stuck_dialog', `Resident on ${hostId}: a permission dialog has been stuck for over ${Math.round(stuckDialogThresholdMs / 60000)} minutes and will never resolve itself; a human must approve or cancel it.`],
264
+ ['provider_error', `Resident on ${hostId}: a provider/credits error was detected in its pane. A human should check its account/billing status.`],
265
+ ];
266
+
267
+ for (const [key, message] of conditionAlerts) {
268
+ const condition = nextState.conditions[key];
269
+ if (shouldSendAlarm(condition, nowMs, schedule)) {
270
+ const sendResult = await sendAlarm({ tgSend, postSlotNotice, text: message, sessionName: `WARDEN-${hostId}`, log });
271
+ nextState.conditions[key] = recordAlarmSent(condition, nowMs);
272
+ try {
273
+ await recordTurn({ outcome: 'error', error_note: `alarm_${key}` });
274
+ } catch (err) {
275
+ log('warn', `warden: alarm receipt failed for ${key}: ${err.message}`);
276
+ }
277
+ report.alerts_sent.push({ condition: key, tg_sent: sendResult.tg_sent });
278
+ }
279
+ }
280
+
281
+ return { state: nextState, ...report };
282
+ }