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,638 @@
1
+ /**
2
+ * Governance commands: policies, WARD tamper-evident chain, VOLT evidence
3
+ * bundles / certification, evidence + trace queries.
4
+ *
5
+ * All endpoints below are wired to real, existing collector routes — see
6
+ * the comment above each subcommand for the exact route it calls.
7
+ *
8
+ * Read-only operations (list/get/status/verify/query) are first-class.
9
+ *
10
+ * Policy WRITE operations (create/update/delete/validate, template packs,
11
+ * approval resolution) were added in WP4.2 (STREAM_CLI_PARITY_PLAN.md §2.3
12
+ * #9) — see the `governance policy`, `governance template`, and
13
+ * `governance approval` subcommands below. `governance certify` remains
14
+ * the one action gated behind an explicit --confirm flag (it creates a
15
+ * permanent VOLT certification record, matching the pattern used by
16
+ * `vault export` and `vault delete`); the policy mutations below do not
17
+ * require --confirm because the backend already enforces its own
18
+ * second-approver (4-eyes) workflow for sensitive changes (a 202 response
19
+ * with `pending_approval` instead of applying immediately).
20
+ */
21
+
22
+ import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
23
+ import { resolveOrgId, parseJSON } from './helpers.js';
24
+ import { resolveEndpoint } from '../api/endpoints.js';
25
+
26
+ export function registerGovernanceCommands(program, ctx) {
27
+ const { client, auth } = ctx;
28
+
29
+ const governance = program
30
+ .command('governance')
31
+ .description('Governance: policies, WARD chain, VOLT evidence + certification');
32
+
33
+ // ── governance policy ───────────────────────────────────────────────────────
34
+ // GET /api/v1/policies?org_id=... (services/collector/server.js)
35
+
36
+ const policy = governance
37
+ .command('policy')
38
+ .description('Manage governance policies');
39
+
40
+ policy
41
+ .command('list')
42
+ .description('List governance policies')
43
+ .option('--category <category>', 'filter by category')
44
+ .option('--enabled <bool>', 'filter by enabled state (true|false)')
45
+ .option('--effect <effect>', 'filter by effect')
46
+ .option('--limit <n>', 'max results', '100')
47
+ .option('--offset <n>', 'result offset', '0')
48
+ .action(async (options) => {
49
+ auth.requireAuth();
50
+ const orgId = resolveOrgId(auth, program);
51
+ const query = { org_id: orgId, limit: options.limit, offset: options.offset };
52
+ if (options.category) query.category = options.category;
53
+ if (options.enabled !== undefined) query.enabled = options.enabled;
54
+ if (options.effect) query.effect = options.effect;
55
+
56
+ const data = await client.get(resolveEndpoint('v1.policies.list').path, { service: resolveEndpoint('v1.policies.list').service, query });
57
+ formatOutput({
58
+ columns: ['id', 'name', 'category', 'effect', 'enabled', 'updated_at'],
59
+ headers: ['ID', 'Name', 'Category', 'Effect', 'Enabled', 'Updated'],
60
+ rows: data.policies || data.data || data,
61
+ }, { format: program.opts().output, emptyMessage: 'No policies found.' });
62
+ });
63
+
64
+ // GET /api/v1/policies/:id
65
+ policy
66
+ .command('get')
67
+ .description('Show policy details')
68
+ .argument('<id>', 'policy ID')
69
+ .action(async (id) => {
70
+ auth.requireAuth();
71
+ const data = await client.get(resolveEndpoint('v1.policies.get', { id }).path, { service: resolveEndpoint('v1.policies.get', { id }).service });
72
+ formatDetail(data.policy || data, { format: program.opts().output, title: `Policy: ${id}` });
73
+ });
74
+
75
+ // GET /api/v1/policies/approvals/pending?org_id=...
76
+ policy
77
+ .command('pending-approvals')
78
+ .description('List policy changes awaiting second-approver sign-off')
79
+ .action(async () => {
80
+ auth.requireAuth();
81
+ const orgId = resolveOrgId(auth, program);
82
+ const data = await client.get(resolveEndpoint('v1.policies.approvals.pending.list').path, {
83
+ service: resolveEndpoint('v1.policies.approvals.pending.list').service,
84
+ query: { org_id: orgId },
85
+ });
86
+ formatOutput({
87
+ columns: ['id', 'policy_id', 'operation', 'actor_id', 'created_at'],
88
+ headers: ['ID', 'Policy', 'Operation', 'Requested By', 'Requested'],
89
+ rows: data.data || data,
90
+ }, { format: program.opts().output, emptyMessage: 'No pending approvals.' });
91
+ });
92
+
93
+ // POST /api/v1/policies — create a new policy (WRITE, WP4.2)
94
+ policy
95
+ .command('create')
96
+ .description('Create a new governance policy')
97
+ .requiredOption('--name <name>', 'policy name')
98
+ .requiredOption('--category <category>', 'policy category')
99
+ .option('--effect <effect>', 'allow | deny | require_approval | redact | log_only', 'deny')
100
+ .option('--priority <n>', 'priority (lower runs first)')
101
+ .option('--scope <json>', 'scope as JSON, e.g. \'{"agents":["*"]}\'')
102
+ .option('--conditions <json>', 'conditions as JSON')
103
+ .option('--enabled <bool>', 'enabled state (true|false)', 'true')
104
+ .option('--reason <text>', 'reason for the policy')
105
+ .option('--framework-mapping <json>', 'framework mapping as a JSON array')
106
+ .option('--audit-required', 'flag this policy as requiring audit')
107
+ .action(async (options) => {
108
+ auth.requireAuth();
109
+ const orgId = resolveOrgId(auth, program);
110
+ const body = {
111
+ org_id: orgId,
112
+ name: options.name,
113
+ category: options.category,
114
+ effect: options.effect,
115
+ enabled: options.enabled === undefined ? true : options.enabled === 'true',
116
+ };
117
+ if (options.priority !== undefined) body.priority = parseInt(options.priority, 10);
118
+ if (options.scope) body.scope = parseJSON(options.scope, '--scope');
119
+ if (options.conditions) body.conditions = parseJSON(options.conditions, '--conditions');
120
+ if (options.reason) body.reason = options.reason;
121
+ if (options.frameworkMapping) body.framework_mapping = parseJSON(options.frameworkMapping, '--framework-mapping');
122
+ if (options.auditRequired) body.audit_required = true;
123
+
124
+ const data = await client.post(resolveEndpoint('v1.policies.create').path, { service: resolveEndpoint('v1.policies.create').service, body });
125
+ printSuccess(`Policy "${options.name}" created (id: ${data.policy?.id}).`);
126
+ formatDetail(data.policy || data, { format: program.opts().output });
127
+ });
128
+
129
+ // PATCH /api/v1/policies/:id — update a policy (WRITE, WP4.2)
130
+ // May return 202 { pending_approval } if the org has opted into 4-eyes
131
+ // review for this change (--enforce-4eyes, or org-level policy).
132
+ policy
133
+ .command('update')
134
+ .description('Update a governance policy (may require second-approver sign-off)')
135
+ .argument('<id>', 'policy ID')
136
+ .option('--name <name>', 'new name')
137
+ .option('--category <category>', 'new category')
138
+ .option('--effect <effect>', 'new effect: allow | deny | require_approval | redact | log_only')
139
+ .option('--priority <n>', 'new priority')
140
+ .option('--scope <json>', 'new scope as JSON')
141
+ .option('--conditions <json>', 'new conditions as JSON')
142
+ .option('--enabled <bool>', 'new enabled state (true|false)')
143
+ .option('--reason <text>', 'reason for the change')
144
+ .option('--enforce-4eyes', 'require second-approver sign-off for this change')
145
+ .action(async (id, options) => {
146
+ auth.requireAuth();
147
+ const body = {};
148
+ if (options.name) body.name = options.name;
149
+ if (options.category) body.category = options.category;
150
+ if (options.effect) body.effect = options.effect;
151
+ if (options.priority !== undefined) body.priority = parseInt(options.priority, 10);
152
+ if (options.scope) body.scope = parseJSON(options.scope, '--scope');
153
+ if (options.conditions) body.conditions = parseJSON(options.conditions, '--conditions');
154
+ if (options.enabled !== undefined) body.enabled = options.enabled === 'true';
155
+ if (options.reason) body.reason_for_change = options.reason;
156
+ if (options.enforce4eyes) body.enforce_4eyes = true;
157
+
158
+ const data = await client.patch(resolveEndpoint('v1.policies.update', { id }).path, { service: resolveEndpoint('v1.policies.update', { id }).service, body });
159
+ if (data.pending_approval) {
160
+ printWarning('Change staged; awaiting second-approver review.');
161
+ formatDetail(data.pending_approval, { format: program.opts().output, title: 'Pending Approval' });
162
+ return;
163
+ }
164
+ printSuccess('Policy updated.');
165
+ formatDetail(data.policy || data, { format: program.opts().output });
166
+ });
167
+
168
+ // DELETE /api/v1/policies/:id — delete a policy (WRITE, WP4.2)
169
+ // May also return 202 { pending_approval } under 4-eyes review.
170
+ policy
171
+ .command('delete')
172
+ .description('Delete a governance policy')
173
+ .argument('<id>', 'policy ID')
174
+ .action(async (id) => {
175
+ auth.requireAuth();
176
+ const data = await client.delete(resolveEndpoint('v1.policies.delete', { id }).path, { service: resolveEndpoint('v1.policies.delete', { id }).service });
177
+ if (data.pending_approval) {
178
+ printWarning('Delete staged; awaiting second-approver review.');
179
+ formatDetail(data.pending_approval, { format: program.opts().output, title: 'Pending Approval' });
180
+ return;
181
+ }
182
+ printSuccess(`Policy ${id} deleted.`);
183
+ });
184
+
185
+ // POST /api/v1/policies/validate — dry-run validation, does not persist (WP4.2)
186
+ policy
187
+ .command('validate')
188
+ .description('Validate a policy definition without saving (dry run)')
189
+ .requiredOption('--name <name>', 'policy name')
190
+ .requiredOption('--category <category>', 'policy category')
191
+ .option('--effect <effect>', 'allow | deny | require_approval | redact | log_only', 'deny')
192
+ .option('--priority <n>', 'priority')
193
+ .option('--scope <json>', 'scope as JSON')
194
+ .option('--conditions <json>', 'conditions as JSON')
195
+ .option('--enabled <bool>', 'enabled state (true|false)', 'true')
196
+ .action(async (options) => {
197
+ auth.requireAuth();
198
+ const body = {
199
+ name: options.name,
200
+ category: options.category,
201
+ effect: options.effect,
202
+ enabled: options.enabled === undefined ? true : options.enabled === 'true',
203
+ };
204
+ if (options.priority !== undefined) body.priority = parseInt(options.priority, 10);
205
+ if (options.scope) body.scope = parseJSON(options.scope, '--scope');
206
+ if (options.conditions) body.conditions = parseJSON(options.conditions, '--conditions');
207
+
208
+ const data = await client.post(resolveEndpoint('v1.policies.validate').path, { service: resolveEndpoint('v1.policies.validate').service, body });
209
+ formatDetail(data, { format: program.opts().output, title: 'Validation Result' });
210
+ if (data.valid === false && program.opts().output !== 'json') {
211
+ printWarning('Validation failed.');
212
+ }
213
+ });
214
+
215
+ // GET /api/v1/policies/history and GET /api/v1/policies/:id/history (WP4.2)
216
+ policy
217
+ .command('history')
218
+ .description('Show policy change history (org-wide, or a single policy with --policy)')
219
+ .option('--policy <id>', 'show history for a single policy ID instead of the whole org')
220
+ .option('--operation <op>', 'filter by operation: created | updated | deleted')
221
+ .option('--actor <id>', 'filter by actor ID')
222
+ .option('--since <iso>', 'filter: since ISO timestamp')
223
+ .option('--until <iso>', 'filter: until ISO timestamp')
224
+ .option('--limit <n>', 'max results')
225
+ .option('--offset <n>', 'result offset')
226
+ .action(async (options) => {
227
+ auth.requireAuth();
228
+ const columns = ['id', 'policy_id', 'operation', 'actor_id', 'created_at'];
229
+ const headers = ['ID', 'Policy', 'Operation', 'Actor', 'Changed'];
230
+
231
+ if (options.policy) {
232
+ const ep = resolveEndpoint('v1.policies.history.get', { id: options.policy });
233
+ const query = {};
234
+ if (options.limit) query.limit = options.limit;
235
+ if (options.offset) query.offset = options.offset;
236
+ const data = await client.get(ep.path, { service: ep.service, query });
237
+ formatOutput({ columns, headers, rows: data.data || data }, { format: program.opts().output, emptyMessage: 'No history found for this policy.' });
238
+ return;
239
+ }
240
+
241
+ const orgId = resolveOrgId(auth, program);
242
+ const ep = resolveEndpoint('v1.policies.history.list');
243
+ const query = { org_id: orgId };
244
+ if (options.operation) query.operation = options.operation;
245
+ if (options.actor) query.actor_id = options.actor;
246
+ if (options.since) query.since = options.since;
247
+ if (options.until) query.until = options.until;
248
+ if (options.limit) query.limit = options.limit;
249
+ if (options.offset) query.offset = options.offset;
250
+ const data = await client.get(ep.path, { service: ep.service, query });
251
+ formatOutput({ columns, headers, rows: data.data || data }, { format: program.opts().output, emptyMessage: 'No policy history found.' });
252
+ });
253
+
254
+ // ── governance template ─────────────────────────────────────────────────────
255
+ // Template packs: pre-built bundles of policies that can be previewed for
256
+ // conflicts and enabled onto an org in one shot. GOV-7 (server.js ~12375).
257
+
258
+ const template = governance
259
+ .command('template')
260
+ .description('Governance policy template packs');
261
+
262
+ // GET /api/v1/policies/templates
263
+ template
264
+ .command('list')
265
+ .description('List available template packs')
266
+ .action(async () => {
267
+ auth.requireAuth();
268
+ const ep = resolveEndpoint('v1.policies.templates.list');
269
+ const data = await client.get(ep.path, { service: ep.service });
270
+ formatOutput({
271
+ columns: ['id', 'name', 'description', 'policyCount'],
272
+ headers: ['ID', 'Name', 'Description', 'Policies'],
273
+ rows: data.data || data,
274
+ }, { format: program.opts().output, emptyMessage: 'No template packs found.' });
275
+ });
276
+
277
+ // GET /api/v1/policies/templates/:packId
278
+ template
279
+ .command('show')
280
+ .description('Show a template pack with its full policy set')
281
+ .argument('<packId>', 'template pack ID')
282
+ .action(async (packId) => {
283
+ auth.requireAuth();
284
+ const ep = resolveEndpoint('v1.policies.templates.get', { packId });
285
+ const data = await client.get(ep.path, { service: ep.service });
286
+ formatDetail(data.pack || data, { format: program.opts().output, title: `Template Pack: ${packId}` });
287
+ });
288
+
289
+ // POST /api/v1/policies/templates/:packId/conflicts — non-destructive preview (WRITE, WP4.2)
290
+ template
291
+ .command('conflicts')
292
+ .description('Check a template pack for conflicts with existing policies before enabling')
293
+ .argument('<packId>', 'template pack ID')
294
+ .action(async (packId) => {
295
+ auth.requireAuth();
296
+ const orgId = resolveOrgId(auth, program);
297
+ const ep = resolveEndpoint('v1.policies.templates.conflicts', { packId });
298
+ const data = await client.post(ep.path, { service: ep.service, body: { org_id: orgId } });
299
+ formatOutput({
300
+ // fleet #392: conflicts return templateName/existingId; `reason` has no counterpart.
301
+ columns: ['templateName', 'existingId'],
302
+ headers: ['Template Policy', 'Conflicts With'],
303
+ rows: data.conflicts || [],
304
+ }, { format: program.opts().output, emptyMessage: 'No conflicts detected.' });
305
+ if (data.hasConflicts && program.opts().output !== 'json') {
306
+ printWarning(`${data.conflicts.length} conflict(s) detected — review before enabling.`);
307
+ }
308
+ });
309
+
310
+ // POST /api/v1/policies/templates/:packId/enable — WRITE, creates policies (WP4.2)
311
+ template
312
+ .command('enable')
313
+ .description('Enable a template pack for the active org (creates its policies)')
314
+ .argument('<packId>', 'template pack ID')
315
+ .option('--created-by <id>', 'attribute the created policies to this actor')
316
+ .action(async (packId, options) => {
317
+ auth.requireAuth();
318
+ const orgId = resolveOrgId(auth, program);
319
+ const body = { org_id: orgId };
320
+ if (options.createdBy) body.created_by = options.createdBy;
321
+ const ep = resolveEndpoint('v1.policies.templates.enable', { packId });
322
+ const data = await client.post(ep.path, { service: ep.service, body });
323
+ printSuccess(`Template pack "${packId}" enabled.`);
324
+ formatDetail(data, { format: program.opts().output });
325
+ });
326
+
327
+ // ── governance approval ─────────────────────────────────────────────────────
328
+ // Second-approver (4-eyes) sign-off on staged policy changes.
329
+
330
+ const approval = governance
331
+ .command('approval')
332
+ .description('Second-approver sign-off on staged policy changes');
333
+
334
+ // POST /api/v1/policies/approvals/:entryId/resolve — WRITE, WP4.2
335
+ approval
336
+ .command('resolve')
337
+ .description('Approve or reject a pending policy change')
338
+ .argument('<entryId>', 'policy history entry ID')
339
+ .requiredOption('--outcome <outcome>', 'approved | rejected')
340
+ .option('--reason <text>', 'reason / comment for the decision')
341
+ .option('--approved-by <id>', 'approver identity (defaults to the logged-in user)')
342
+ .action(async (entryId, options) => {
343
+ auth.requireAuth();
344
+ if (!['approved', 'rejected'].includes(options.outcome)) {
345
+ throw new Error("--outcome must be 'approved' or 'rejected'");
346
+ }
347
+ const tokens = auth.getTokens ? auth.getTokens() : null;
348
+ const approvedBy = options.approvedBy || tokens?.email || tokens?.user_id;
349
+ if (!approvedBy) {
350
+ throw new Error('--approved-by required (no stored user identity found; run `quox login` or pass --approved-by).');
351
+ }
352
+ const body = { outcome: options.outcome, approved_by: approvedBy };
353
+ if (options.reason) body.reason = options.reason;
354
+
355
+ const ep = resolveEndpoint('v1.policies.approvals.resolve', { entryId });
356
+ const data = await client.post(ep.path, { service: ep.service, body });
357
+ printSuccess(`Approval ${entryId} ${options.outcome}.`);
358
+ formatDetail(data.entry || data, { format: program.opts().output });
359
+ });
360
+
361
+ // ── governance ward ─────────────────────────────────────────────────────────
362
+ // Mounted via services/ward/api-cjs.cjs mountWardRoutes(app) directly on
363
+ // the collector app (no org-scoped path prefix).
364
+
365
+ const ward = governance
366
+ .command('ward')
367
+ .description('WARD tamper-evident chain');
368
+
369
+ // GET /ward/stats
370
+ ward
371
+ .command('status')
372
+ .description('Show WARD chain + witness statistics')
373
+ .action(async () => {
374
+ auth.requireAuth();
375
+ const data = await client.get(resolveEndpoint('ward.stats.list').path, { service: resolveEndpoint('ward.stats.list').service });
376
+ formatDetail({ ...data.stats, ...data.witness }, { format: program.opts().output, title: 'WARD Status' });
377
+ });
378
+
379
+ // GET /ward/chains
380
+ ward
381
+ .command('chains')
382
+ .description('List WARD chains')
383
+ .action(async () => {
384
+ auth.requireAuth();
385
+ const data = await client.get(resolveEndpoint('ward.chains.list').path, { service: resolveEndpoint('ward.chains.list').service });
386
+ formatOutput({
387
+ columns: ['chain_id', 'created_at'],
388
+ headers: ['Chain ID', 'Created'],
389
+ rows: data.chains || data,
390
+ }, { format: program.opts().output, emptyMessage: 'No chains found.' });
391
+ });
392
+
393
+ // GET /ward/entries?chain=...
394
+ ward
395
+ .command('entries')
396
+ .description('List entries in a WARD chain')
397
+ .option('--chain <chainId>', 'chain ID (default: production chain)')
398
+ .option('--limit <n>', 'max results', '50')
399
+ .action(async (options) => {
400
+ auth.requireAuth();
401
+ const query = { limit: options.limit };
402
+ if (options.chain) query.chain = options.chain;
403
+ const data = await client.get(resolveEndpoint('ward.entries.list').path, { service: resolveEndpoint('ward.entries.list').service, query });
404
+ formatOutput({
405
+ // fleet #392: WARD entries carry witnessed_at, not created_at.
406
+ columns: ['seq', 'source_kind', 'chain_id', 'witnessed_at'],
407
+ headers: ['Seq', 'Source', 'Chain', 'Created'],
408
+ rows: data.entries || data,
409
+ }, { format: program.opts().output, emptyMessage: 'No entries found.' });
410
+ });
411
+
412
+ // POST /ward/verify — non-mutating integrity check, safe to run without --confirm
413
+ ward
414
+ .command('verify')
415
+ .description('Verify a WARD chain\'s integrity')
416
+ // #622: --chain is no longer REQUIRED. An auditor who ran `ward verify`
417
+ // with nothing hit "error: required option --chain" with no way to discover
418
+ // a chain id from here, so verify dead-ended. Now bare `verify` LISTS the
419
+ // discoverable chains + how to pick one, instead of erroring.
420
+ //
421
+ // A `--all` that verified every chain is still NOT kept, but the underlying
422
+ // hazard is now fixed (GTM-G3d, 2026-09-28): server-side verify is BOUNDED, so
423
+ // verifying the production chain (millions of entries) no longer pegs the
424
+ // collector — it returns PARTIAL/BOUNDED_LARGE_CHAIN over the head window in
425
+ // seconds. For full coverage of a very large chain, page with
426
+ // --from-seq/--up-to-seq (bounded per call). Still one explicit chain at a time.
427
+ .option('--chain <chainId>', 'chain ID to verify (see `quox governance ward chains`)')
428
+ .option('--from-seq <n>', 'verify from this seq (page a large chain; bounded per call)', (v) => parseInt(v, 10))
429
+ .option('--up-to-seq <n>', 'verify up to this seq (page a large chain; bounded per call)', (v) => parseInt(v, 10))
430
+ // #622: prove ONE action end-to-end. An auditor asked "prove this agent
431
+ // action is in your tamper-evident log" had no CLI path; now they take the
432
+ // envelope id from `quox aee get <ENV_...>` and verify its receipt. Bounded
433
+ // (entry → covering signed tip), so it is safe on the production chain that
434
+ // a full --chain verify is not. --source/--source-kind generalise it to
435
+ // AOCL/VOLT/EXTERNAL source events.
436
+ .option('--envelope <envelopeId>', 'verify one AEE envelope\'s inclusion receipt (alias for --source <id> --source-kind AEE)')
437
+ .option('--source <sourceId>', 'verify one source event\'s inclusion receipt')
438
+ .option('--source-kind <kind>', 'source kind for --source (AEE|AOCL|VOLT|WARD|EXTERNAL)', 'AEE')
439
+ .action(async (options) => {
440
+ auth.requireAuth();
441
+ const isJson = program.opts().output === 'json';
442
+
443
+ // #622: single-action receipt path — takes precedence over chain verify.
444
+ const sourceId = options.envelope || options.source;
445
+ if (sourceId) {
446
+ const sourceKind = options.envelope ? 'AEE' : options.sourceKind;
447
+ const data = await client.post(resolveEndpoint('ward.verify_receipt').path, {
448
+ service: resolveEndpoint('ward.verify_receipt').service,
449
+ body: { source_id: sourceId, source_kind: sourceKind, chain_id: options.chain || undefined },
450
+ });
451
+ const result = data.result || data;
452
+ formatDetail(result, { format: program.opts().output, title: `WARD Receipt: ${sourceKind} ${sourceId}` });
453
+ if (result.status !== 'WITNESSED' && !isJson) {
454
+ printWarning(`Receipt status: ${result.status}${result.reason ? ` (${result.reason})` : ''}`);
455
+ }
456
+ if (result.status !== 'WITNESSED') process.exitCode = 1;
457
+ return;
458
+ }
459
+
460
+ if (!options.chain) {
461
+ // Kill the dead-end: instead of erroring, show what CAN be verified.
462
+ const chainsData = await client.get(resolveEndpoint('ward.chains.list').path, { service: resolveEndpoint('ward.chains.list').service });
463
+ const chains = chainsData.chains || chainsData || [];
464
+ if (isJson) { console.log(JSON.stringify({ error: 'chain_required', hint: 'pass --chain <id>', chains }, null, 2)); process.exitCode = 1; return; }
465
+ printWarning('Specify --chain <id> to verify a chain. Discoverable chains:');
466
+ formatOutput({
467
+ columns: ['chain_id', 'created_at'],
468
+ headers: ['Chain ID', 'Created'],
469
+ rows: chains,
470
+ }, { format: program.opts().output, emptyMessage: 'No chains found.' });
471
+ process.exitCode = 1;
472
+ return;
473
+ }
474
+
475
+ const verifyBody = { chain_id: options.chain };
476
+ if (Number.isFinite(options.fromSeq)) verifyBody.from_seq = options.fromSeq;
477
+ if (Number.isFinite(options.upToSeq)) verifyBody.up_to_seq = options.upToSeq;
478
+ const data = await client.post(resolveEndpoint('ward.verify').path, {
479
+ service: resolveEndpoint('ward.verify').service,
480
+ body: verifyBody,
481
+ });
482
+ const result = data.result || data;
483
+ formatDetail(result, { format: program.opts().output, title: `WARD Verify: ${options.chain}` });
484
+ // G3d: a bounded verify of a large chain returns PARTIAL/BOUNDED_LARGE_CHAIN
485
+ // (or PARTIAL/WINDOWED when paging) — that is a HEALTHY result, not a failure.
486
+ // Treat those as success (exit 0); only a genuine problem (BROKEN, or PARTIAL
487
+ // for INCOMPLETE_CHAIN / WINDOW_TOO_LARGE / a real gap) is exit 1.
488
+ const boundedOk = result.status === 'PARTIAL'
489
+ && (result.reason === 'BOUNDED_LARGE_CHAIN' || result.reason === 'WINDOWED');
490
+ const healthy = result.status === 'INTACT' || boundedOk;
491
+ if (result.status && !healthy && !isJson) {
492
+ printWarning(`Chain status: ${result.status}${result.reason ? ` (${result.reason})` : ''}`);
493
+ } else if (boundedOk && !isJson) {
494
+ printWarning(`Chain status: verified bounded (${result.verified_range ? `seq ${result.verified_range.from}..${result.verified_range.to} of ${result.entry_count}` : 'head window'}); page with --from-seq/--up-to-seq for full coverage.`);
495
+ }
496
+ if (result.status && !healthy) process.exitCode = 1;
497
+ });
498
+
499
+ // ── governance evidence ──────────────────────────────────────────────────────
500
+ // GET /quoxflow/evidence — collector proxy to quoxflow GET /api/evidence
501
+
502
+ governance
503
+ .command('evidence')
504
+ .description('Query VOLT/quoxflow evidence records')
505
+ .option('--corr <id>', 'correlation ID')
506
+ .option('--envelope <id>', 'envelope ID')
507
+ .option('--workflow <id>', 'workflow ID')
508
+ .option('--execution <id>', 'execution ID')
509
+ .option('--type <type>', 'evidence type')
510
+ .option('--limit <n>', 'max results')
511
+ .option('--offset <n>', 'result offset')
512
+ .action(async (options) => {
513
+ auth.requireAuth();
514
+ const query = {};
515
+ if (options.corr) query.corr = options.corr;
516
+ if (options.envelope) query.envelopeId = options.envelope;
517
+ if (options.workflow) query.workflowId = options.workflow;
518
+ if (options.execution) query.executionId = options.execution;
519
+ if (options.type) query.type = options.type;
520
+ if (options.limit) query.limit = options.limit;
521
+ if (options.offset) query.offset = options.offset;
522
+
523
+ const data = await client.get(resolveEndpoint('quoxflow.evidence.list').path, { service: resolveEndpoint('quoxflow.evidence.list').service, query });
524
+ formatOutput({
525
+ // fleet #392: quoxflow evidence is camelCase (createdAt).
526
+ columns: ['id', 'type', 'correlationId', 'workflowId', 'createdAt'],
527
+ headers: ['ID', 'Type', 'Correlation', 'Workflow', 'Created'],
528
+ rows: data.evidence || data.data || data,
529
+ }, { format: program.opts().output, emptyMessage: 'No evidence records found.' });
530
+ });
531
+
532
+ // ── governance traces ─────────────────────────────────────────────────────────
533
+ // GET /quoxflow/observations — collector proxy to quoxflow GET /api/observations
534
+
535
+ governance
536
+ .command('traces')
537
+ .description('Query quoxflow execution traces (observations)')
538
+ .option('--task <id>', 'task ID')
539
+ .option('--correlation <id>', 'correlation ID')
540
+ .option('--workflow <id>', 'workflow ID')
541
+ .option('--org <id>', 'org ID (overrides active org)')
542
+ .action(async (options) => {
543
+ auth.requireAuth();
544
+ const query = {};
545
+ if (options.task) query.taskId = options.task;
546
+ if (options.correlation) query.correlationId = options.correlation;
547
+ if (options.workflow) query.workflowId = options.workflow;
548
+ query.orgId = options.org || resolveOrgId(auth, program);
549
+
550
+ const data = await client.get(resolveEndpoint('quoxflow.observations.list').path, { service: resolveEndpoint('quoxflow.observations.list').service, query });
551
+ formatOutput({
552
+ // fleet #392: observations carry ts, not created_at.
553
+ columns: ['id', 'taskId', 'correlationId', 'ts'],
554
+ headers: ['ID', 'Task', 'Correlation', 'Created'],
555
+ rows: data.observations || data.data || data,
556
+ }, { format: program.opts().output, emptyMessage: 'No traces found.' });
557
+ });
558
+
559
+ // ── governance certify ───────────────────────────────────────────────────────
560
+ // POST /volt/certify — the one write. Requires --confirm.
561
+
562
+ governance
563
+ .command('certify')
564
+ .description('Run VOLT certification against a run (WRITE — creates a certification record)')
565
+ .argument('<runId>', 'VOLT run ID')
566
+ .option('--retention-days <n>', 'retention period in days')
567
+ .option('--audit-info <json>', 'audit info as JSON')
568
+ .option('--compliance-info <json>', 'compliance info as JSON')
569
+ .option('--confirm', 'confirm you want to run certification')
570
+ .action(async (runId, options) => {
571
+ auth.requireAuth();
572
+ if (!options.confirm) {
573
+ throw new Error('Certification creates a permanent record. Add --confirm to proceed.');
574
+ }
575
+ const body = { run_id: runId };
576
+ if (options.retentionDays) body.retention_days = parseInt(options.retentionDays, 10);
577
+ if (options.auditInfo) body.audit_info = parseJSON(options.auditInfo, '--audit-info');
578
+ if (options.complianceInfo) body.compliance_info = parseJSON(options.complianceInfo, '--compliance-info');
579
+
580
+ const data = await client.post(resolveEndpoint('volt.certify').path, { service: resolveEndpoint('volt.certify').service, body });
581
+ printSuccess(`Certification created: ${data.cert_id} (tier: ${data.tier_name || data.tier})`);
582
+ formatDetail(data, { format: program.opts().output });
583
+ });
584
+
585
+ // ── governance cert ──────────────────────────────────────────────────────────
586
+ // GET /volt/cert/:certId, /volt/cert/run/:runId, /volt/certs, /volt/certs/stats
587
+
588
+ const cert = governance
589
+ .command('cert')
590
+ .description('Inspect VOLT certifications');
591
+
592
+ cert
593
+ .command('get')
594
+ .description('Get a certification by ID')
595
+ .argument('<certId>', 'certification ID')
596
+ .action(async (certId) => {
597
+ auth.requireAuth();
598
+ const data = await client.get(resolveEndpoint('volt.cert.get', { certId }).path, { service: resolveEndpoint('volt.cert.get', { certId }).service });
599
+ formatDetail(data.cert || data, { format: program.opts().output, title: `Certification: ${certId}` });
600
+ });
601
+
602
+ cert
603
+ .command('for-run')
604
+ .description('Get the latest certification for a run')
605
+ .argument('<runId>', 'VOLT run ID')
606
+ .action(async (runId) => {
607
+ auth.requireAuth();
608
+ const data = await client.get(resolveEndpoint('volt.cert.run.get', { runId }).path, { service: resolveEndpoint('volt.cert.run.get', { runId }).service });
609
+ formatDetail(data.cert || data, { format: program.opts().output, title: `Certification for run: ${runId}` });
610
+ });
611
+
612
+ cert
613
+ .command('list')
614
+ .description('List certifications')
615
+ .option('--limit <n>', 'max results', '30')
616
+ .option('--offset <n>', 'result offset', '0')
617
+ .action(async (options) => {
618
+ auth.requireAuth();
619
+ const data = await client.get(resolveEndpoint('volt.certs.list').path, {
620
+ service: resolveEndpoint('volt.certs.list').service,
621
+ query: { limit: options.limit, offset: options.offset },
622
+ });
623
+ formatOutput({
624
+ columns: ['cert_id', 'run_id', 'tier', 'tier_name', 'certified_at', 'expires_at'],
625
+ headers: ['Cert ID', 'Run', 'Tier', 'Tier Name', 'Certified', 'Expires'],
626
+ rows: data.certs || data,
627
+ }, { format: program.opts().output, emptyMessage: 'No certifications found.' });
628
+ });
629
+
630
+ cert
631
+ .command('stats')
632
+ .description('Certification statistics')
633
+ .action(async () => {
634
+ auth.requireAuth();
635
+ const data = await client.get(resolveEndpoint('volt.certs.stats.list').path, { service: resolveEndpoint('volt.certs.stats.list').service });
636
+ formatDetail(data, { format: program.opts().output, title: 'Certification Stats' });
637
+ });
638
+ }