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,170 @@
1
+ /**
2
+ * Data for the inline object views, from the real services only. Every read is bounded (timeouts are
3
+ * short on purpose: an inline card must not hang the composer) and every failure comes back as a
4
+ * reason string, never as an empty result. Status is checked before anything is parsed: the client
5
+ * throws on non-2xx, and the throw is what becomes the reason.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { findSkill } from '../skills.js';
12
+ import { packFrom } from './ladder.js';
13
+ import { slotFor } from './agent.js';
14
+
15
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
16
+ const ASSISTANTS_DIR = path.resolve(HERE, '../../../../src/config/entities/assistants');
17
+ const listOf = (d, ...keys) => { for (const k of keys) if (Array.isArray(d?.[k])) return d[k]; return Array.isArray(d) ? d : []; };
18
+ const why = (e) => (e?.status ? `HTTP ${e.status}` : /timed out/i.test(e?.message || '') ? 'timed out' : e?.message || 'failed');
19
+
20
+ /** The shipped assistant identities: id -> { name, tagline, description }. */
21
+ export function bundledIdentities(dir = ASSISTANTS_DIR) {
22
+ const out = new Map();
23
+ let files = [];
24
+ try { files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); } catch { return out; }
25
+ for (const f of files) {
26
+ try {
27
+ const j = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
28
+ if (j?.id) out.set(j.id, { name: j.identity?.name || j.id, tagline: j.identity?.tagline || null, description: j.identity?.description || null });
29
+ } catch { /* skip unreadable */ }
30
+ }
31
+ return out;
32
+ }
33
+
34
+ export function createObjectsApi({ client, orgId = () => null, home = os.homedir(), identities = bundledIdentities }) {
35
+ const get = (service, p, query, timeout = 12000) => client.get(p, { service, query, retries: 0, timeout });
36
+ const post = (service, p, body, timeout = 8000) => client.post(p, { service, body, retries: 0, timeout });
37
+ const org = () => { const o = orgId(); return typeof o === 'string' ? o : null; };
38
+ let ids = null;
39
+ const idents = () => (ids ??= identities());
40
+ const nameOf = (id) => idents().get(id)?.name || id;
41
+
42
+ async function verifyReceipt(envelopeId, timeoutS = 8) {
43
+ try { const d = await post('collector', '/ward/verify-receipt', { source_id: envelopeId, source_kind: 'AEE' }, timeoutS * 1000); return { ok: true, result: d?.result || d }; }
44
+ catch (e) { return { ok: false, error: why(e), timeout: /timed out/i.test(e?.message || '') ? timeoutS : null }; }
45
+ }
46
+ async function aeeGet(id) {
47
+ try { const d = await get('collector', `/aee/${encodeURIComponent(id)}`, undefined, 10000); return { ok: true, env: d?.envelope || d }; }
48
+ catch (e) { return { ok: false, status: e?.status, error: why(e) }; }
49
+ }
50
+
51
+ /** Workflow by id or display name (quoxflow GET /api/workflows). -> { id, name, definition } | null */
52
+ async function findWorkflow(ref) {
53
+ const r = String(ref || '').trim();
54
+ try { const d = await get('quoxflow', `/api/workflows/${encodeURIComponent(r)}`); const w = d?.data || d; if (w?.id) return w; } catch (e) { if (e?.status && e.status !== 404) throw e; }
55
+ const all = listOf(await get('quoxflow', '/api/workflows', { limit: 100 }), 'data', 'workflows', 'items');
56
+ const lc = r.toLowerCase();
57
+ return all.find(w => String(w.name || '').toLowerCase() === lc) || all.find(w => String(w.name || '').toLowerCase().includes(lc)) || null;
58
+ }
59
+ async function pageExecutions(workflowId, status, max = 500) {
60
+ const out = [];
61
+ for (let offset = 0; offset < max; offset += 100) {
62
+ const rows = listOf(await get('quoxflow', '/api/executions', { workflowId, status, limit: 100, offset }), 'data', 'executions');
63
+ out.push(...rows);
64
+ if (rows.length < 100) break;
65
+ }
66
+ return out;
67
+ }
68
+
69
+ return {
70
+ /** E1 */
71
+ async skill(name) {
72
+ const all = listOf(await get('collector', '/quoxflow/skills', { orgId: org() || undefined }), 'data', 'skills');
73
+ const hit = findSkill(all, name);
74
+ if (!hit) return { error: `No skill named "${name}". /skills lists them.`, tone: 'warn' };
75
+ let skill = hit;
76
+ try { const d = await get('collector', `/quoxflow/skills/${encodeURIComponent(hit.id)}`, { orgId: org() || undefined }); skill = d?.data || d || hit; } catch { /* the list row is the same record */ }
77
+ let scoreboard = null;
78
+ // The org-scoped per-skill route; 404 = no recorded runs. (The bulk /scoreboards list is not
79
+ // org-filtered in quoxflow, so it is deliberately not used here.)
80
+ try { const d = await get('quoxflow', `/api/quox-skills/${encodeURIComponent(skill.id)}/scoreboard`, undefined, 8000); scoreboard = { ok: true, row: d?.data || null }; }
81
+ catch (e) { scoreboard = e?.status === 404 ? { ok: true, row: null } : { ok: false, error: why(e) }; }
82
+ return { data: { skill, scoreboard, agentName: nameOf } };
83
+ },
84
+
85
+ /** E2: ref = execution id / correlation id, or a workflow id or name (its latest run). */
86
+ async workflowRun(ref) {
87
+ let execution = null;
88
+ try { const d = await get('quoxflow', `/api/executions/${encodeURIComponent(ref)}`); execution = d?.data || d; } catch (e) { if (e?.status && e.status !== 404) throw e; }
89
+ if (!execution?.id) {
90
+ const wf = await findWorkflow(ref);
91
+ if (!wf) return { error: `No workflow or run called "${ref}".`, tone: 'warn' };
92
+ const rows = listOf(await get('quoxflow', '/api/executions', { workflowId: wf.id, limit: 1 }), 'data', 'executions');
93
+ if (!rows.length) return { error: `${wf.name || wf.id} has never run.`, tone: 'warn' };
94
+ execution = rows[0];
95
+ }
96
+ let envelopes = null, def = null;
97
+ try { const d = await get('quoxflow', `/api/executions/${encodeURIComponent(execution.id)}/envelopes`); envelopes = listOf(d?.data || d, 'envelopes'); } catch { envelopes = null; }
98
+ try { const d = await get('quoxflow', `/api/workflows/${encodeURIComponent(execution.workflowId)}`); def = (d?.data || d)?.definition || null; } catch { def = null; }
99
+ return { data: { execution, envelopes, def } };
100
+ },
101
+
102
+ /** E3 */
103
+ async crystal(ref) {
104
+ const wf = await findWorkflow(ref);
105
+ if (!wf) return { error: `No workflow called "${ref}".`, tone: 'warn' };
106
+ const [completed, failed] = await Promise.all([pageExecutions(wf.id, 'completed'), pageExecutions(wf.id, 'failed')]);
107
+ return { data: { workflow: { id: wf.id, name: wf.name }, executions: [...completed, ...failed], def: wf.definition || null } };
108
+ },
109
+
110
+ /** E4 */
111
+ async agent(name) {
112
+ const q = String(name || '').trim().toLowerCase();
113
+ let toolMap = null, toolErr = null;
114
+ try { toolMap = (await client.get('/api/v1/tools/agents', { service: 'collector', noAuth: true, retries: 0, timeout: 10000 }))?.agentTools || null; } catch (e) { toolErr = why(e); }
115
+ let orgAgents = [];
116
+ try { if (org()) orgAgents = listOf(await get('auth', `/api/orgs/${org()}/agents`, { includeTools: 'false' }), 'agents', 'data'); } catch { /* optional */ }
117
+ const known = new Set([...Object.keys(toolMap || {}), ...idents().keys(), ...orgAgents.map(a => a.agent_id)]);
118
+ let id = [...known].find(k => k.toLowerCase() === q);
119
+ if (!id) id = [...known].find(k => String(nameOf(k)).toLowerCase() === q) || orgAgents.find(a => String(a.name || '').toLowerCase() === q)?.agent_id;
120
+ if (!id) return { error: `No agent called "${name}".${toolErr ? ` (agent list: ${toolErr})` : ''}`, tone: 'warn' };
121
+ const record = orgAgents.find(a => a.agent_id === id) || null;
122
+ const ident = idents().get(id) || {};
123
+ let slot = null;
124
+ try { slot = slotFor(listOf(await get('collector', '/api/v1/slots', undefined, 10000), 'slots'), id); } catch { slot = null; }
125
+ let last = null, lastErr = null, receipt = null;
126
+ try { last = listOf(await get('collector', '/aee/query', { from: `agent.${id}`, limit: 1 }, 10000), 'envelopes')[0] || null; } catch (e) { lastErr = why(e); }
127
+ if (last?.id) receipt = await verifyReceipt(last.id);
128
+ return { data: { id, name: ident.name || record?.name || id, tagline: ident.tagline, description: record?.description || ident.description || null, status: record?.status || null, record, tools: toolMap ? (toolMap[id] || []) : null, toolsErr: toolErr, slot, last, lastErr, receipt } };
129
+ },
130
+
131
+ /** E6 */
132
+ async memory(query) {
133
+ const d = await get('memory', '/api/memory/search', { q: query, limit: 10 });
134
+ return { data: { query, results: listOf(d, 'results', 'memories', 'data') } };
135
+ },
136
+
137
+ /** E7 */
138
+ async rules() {
139
+ let policies;
140
+ const o = org();
141
+ if (!o) policies = { ok: false, error: 'no org on this profile' };
142
+ else {
143
+ try { policies = { ok: true, list: listOf(await get('collector', `/orgs/${o}/governance/policies`, undefined, 15000), 'policies') }; }
144
+ catch (e) { policies = { ok: false, error: why(e) }; }
145
+ }
146
+ const pack = packFrom((f) => fs.readFileSync(path.join(home, '.claude', 'skills', f), 'utf8'));
147
+ return { data: { policies, pack } };
148
+ },
149
+
150
+ /** D1 */
151
+ async proof(tool) {
152
+ const envelope = await aeeGet(tool.envelopeId);
153
+ let aocl = null;
154
+ const corr = envelope.ok ? envelope.env?.corr : null;
155
+ if (corr) {
156
+ try { aocl = { ok: true, list: listOf(await get('collector', '/aee/query', { corr, limit: 50 }, 10000), 'envelopes').filter(e => /^aocl\./.test(String(e.intent || ''))) }; }
157
+ catch (e) { aocl = { ok: false, error: why(e) }; }
158
+ }
159
+ const receipt = await verifyReceipt(tool.envelopeId);
160
+ return { data: { tool, envelope, aocl, receipt } };
161
+ },
162
+
163
+ /** D3 */
164
+ async reconcile(tools) {
165
+ const items = [];
166
+ for (const t of tools) items.push({ tool: t.tool, envelopeId: t.envelopeId || null, record: t.envelopeId ? await aeeGet(t.envelopeId) : null });
167
+ return { data: { items } };
168
+ }
169
+ };
170
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Inline object views: one entry point for app.js. `runObjectCommand` fetches and returns either an
3
+ * object item ({ type, data }) that history renders with <ObjectView/>, or a note ({ text, tone }).
4
+ * The view itself is rebuilt from data at render width (resize reprints history), so items stay
5
+ * pure data.
6
+ */
7
+ import { skillCard } from './skill.js';
8
+ import { stepFlow } from './workflow.js';
9
+ import { crystalCard } from './crystal.js';
10
+ import { agentCard } from './agent.js';
11
+ import { memoryFacts } from './memory.js';
12
+ import { rulesLadder } from './ladder.js';
13
+ import { proofChain, reconcileView } from './proof.js';
14
+
15
+ export { skillCard, stepFlow, crystalCard, agentCard, memoryFacts, rulesLadder, proofChain, reconcileView };
16
+ export { createObjectsApi } from './fetch.js';
17
+ export { decodeRow, arrivalSeconds } from './model.js';
18
+
19
+ export const BUILDERS = { skill: skillCard, workflow: stepFlow, crystal: crystalCard, agent: agentCard, memory: memoryFacts, rules: rulesLadder, proof: proofChain, reconcile: reconcileView };
20
+
21
+ /** -> view { box?, rows } for an object item at this width (now injected for tests). */
22
+ export function buildObject(obj, cols, now = Date.now()) {
23
+ const b = BUILDERS[obj?.type];
24
+ if (!b) return { rows: [] };
25
+ return b({ now, ...obj.data }, cols);
26
+ }
27
+
28
+ /** Tool blocks of this session that carry an envelope id, newest last. */
29
+ export function toolCalls(items) {
30
+ return (items || []).filter(b => b && b.kind === 'tool' && b.status !== 'running');
31
+ }
32
+
33
+ const USAGE = {
34
+ '/workflow': 'Usage: /workflow <name | workflow id | run id>',
35
+ '/crystal': 'Usage: /crystal <workflow name or id>',
36
+ '/memory': 'Usage: /memory <query>',
37
+ '/agents': 'Usage: /agents <name>'
38
+ };
39
+
40
+ /**
41
+ * cmd: '/skills' (with a name), '/workflow', '/crystal', '/agents' (with a name), '/memory', '/rules',
42
+ * '/proof', '/reconcile'. ctx: { objects (createObjectsApi), rules, rulesOn, items (history + live
43
+ * blocks) }. -> { object: { type, data } } | { note: text, tone } | null (not ours).
44
+ */
45
+ export async function runObjectCommand(cmd, arg, ctx) {
46
+ const o = ctx.objects;
47
+ const a = String(arg || '').trim();
48
+ // A bare /skills or /agents is a list, not an object: leave it to its own handler.
49
+ if (!['/workflow', '/crystal', '/memory', '/rules', '/proof', '/reconcile'].includes(cmd) && !((cmd === '/skills' || cmd === '/agents') && a)) return null;
50
+ if (!o) return cmd === '/rules' || cmd === '/skills' ? null : { note: 'Object views are not available in this mode.', tone: 'warn' };
51
+ const wrap = async (type, fn) => {
52
+ try {
53
+ const r = await fn();
54
+ if (r?.error) return { note: r.error, tone: r.tone || 'warn' };
55
+ return { object: { type, data: r.data } };
56
+ } catch (e) {
57
+ return { note: `${type}: ${e?.status ? `HTTP ${e.status}: ` : ''}${e?.message || 'failed'}`, tone: 'warn' };
58
+ }
59
+ };
60
+ switch (cmd) {
61
+ case '/skills': return a ? wrap('skill', () => o.skill(a)) : null;
62
+ case '/agents': return a ? wrap('agent', () => o.agent(a)) : null;
63
+ case '/workflow': return a ? wrap('workflow', () => o.workflowRun(a)) : { note: USAGE['/workflow'], tone: 'warn' };
64
+ case '/crystal': return a ? wrap('crystal', () => o.crystal(a)) : { note: USAGE['/crystal'], tone: 'warn' };
65
+ case '/memory': return a ? wrap('memory', () => o.memory(a)) : { note: USAGE['/memory'], tone: 'warn' };
66
+ case '/rules': {
67
+ return wrap('rules', async () => {
68
+ const r = await o.rules();
69
+ return { data: { ...r.data, quoxmd: ctx.rules || null, enabled: ctx.rulesOn !== false } };
70
+ });
71
+ }
72
+ case '/proof': {
73
+ const calls = toolCalls(ctx.items).filter(b => b.envelopeId);
74
+ const t = a ? calls.find(b => b.envelopeId === a || b.callId === a) : calls[calls.length - 1];
75
+ if (!t) return { note: a ? `No tool call "${a}" with an envelope in this session.` : 'No tool call with an envelope id in this session yet.', tone: 'warn' };
76
+ return wrap('proof', () => o.proof({ tool: t.tool, status: t.status, envelopeId: t.envelopeId, callId: t.callId }));
77
+ }
78
+ case '/reconcile': {
79
+ const unk = toolCalls(ctx.items).filter(b => b.status === 'effect_unknown');
80
+ if (!unk.length) return { note: 'Nothing to reconcile: no tool call in this session ended with effect unknown.' };
81
+ return wrap('reconcile', () => o.reconcile(unk.map(b => ({ tool: b.tool, envelopeId: b.envelopeId }))));
82
+ }
83
+ default: return null;
84
+ }
85
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * E7 rules precedence: three layers that are visibly different and never confused.
3
+ * advisory QUOX.md files this client found (sent as context; cannot grant or bypass anything)
4
+ * pack the signed core dev pack on this machine (~/.claude/skills/.pack-version and
5
+ * .pack-rights.json, written by the collector's skill-pack installer): team guidance for
6
+ * coding agents, still advisory
7
+ * enforced ARMED governance policies on the server (GET /orgs/:org/governance/policies): the only
8
+ * layer that can stop an action
9
+ * The effective layer is the highest one that has rules. An unreadable layer is reported, and never
10
+ * treated as empty when deciding what is effective.
11
+ */
12
+ import { seg, fit, plural } from './model.js';
13
+
14
+ /** Pure: read the pack marker files through an injected reader (fs.readFileSync-like, may throw). */
15
+ export function packFrom(read) {
16
+ let marker = null, rights = null;
17
+ try { marker = String(read('.pack-version')).trim(); } catch { /* absent */ }
18
+ try { rights = JSON.parse(read('.pack-rights.json')); } catch { /* absent */ }
19
+ if (!marker && !rights) return { state: 'none' };
20
+ let m = null;
21
+ try { m = marker ? JSON.parse(marker) : null; } catch { m = null; }
22
+ const version = rights?.version ?? m?.version ?? (marker && !m ? marker : null);
23
+ return { state: 'installed', version: version != null ? String(version) : null, packId: rights?.packId ?? m?.packId ?? null, skills: Array.isArray(rights?.rights) ? rights.rights.length : null };
24
+ }
25
+
26
+ /**
27
+ * d: { quoxmd: loadRules() result | null, enabled: boolean, pack: packFrom() result,
28
+ * policies: { ok: true, list } | { ok: false, error } }
29
+ */
30
+ export function rulesLadder(d, cols) {
31
+ const nw = cols < 66;
32
+ const rows = [];
33
+ const files = d.quoxmd?.files || [];
34
+ const pol = d.policies || { ok: false, error: 'not read' };
35
+ const armed = pol.ok ? (pol.list || []).filter(p => p.state === 'armed') : [];
36
+ const staged = pol.ok ? (pol.list || []).filter(p => p.state === 'draft') : [];
37
+ const layers = [
38
+ { tag: 'advisory', role: 'muted', has: files.length > 0 && d.enabled !== false,
39
+ src: !files.length ? 'no QUOX.md found' : d.enabled === false ? `QUOX.md (${files.length}) · off this session` : `QUOX.md (${plural(files.length, 'file')}${d.quoxmd?.truncated ? ', cut' : ''})`,
40
+ scope: nw ? 'local' : 'you, this machine · guides only' },
41
+ { tag: 'pack', role: 'teal', has: d.pack?.state === 'installed',
42
+ src: d.pack?.state === 'installed' ? `core dev pack${d.pack.version ? ` v${d.pack.version}` : ''}${d.pack.skills != null && !nw ? ` · ${d.pack.skills} skills` : ''}` : 'not installed here',
43
+ scope: nw ? 'team' : 'team, signed · guides only' },
44
+ { tag: 'enforced', role: 'indigo', has: armed.length > 0, unknown: !pol.ok,
45
+ src: pol.ok ? (armed.length ? `${plural(armed.length, 'governance policy', 'governance policies')} armed` : 'no armed policy') : `could not read (${fit(pol.error, 28)})`,
46
+ scope: nw ? 'server' : 'server, cannot be bypassed' }
47
+ ];
48
+ const top = pol.ok ? [...layers].reverse().find(l => l.has) : null;
49
+ rows.push([seg('rules ', 'muted'), seg(nw ? 'precedence, top wins' : 'precedence · the highest layer with rules wins', 'value', true)]);
50
+ for (const l of [...layers].reverse()) {
51
+ const on = top === l;
52
+ const lose = top && !on && l.has;
53
+ rows.push([
54
+ seg(on ? '▸ ' : ' ', on ? 'value' : 'faint', true),
55
+ seg(l.tag.padEnd(9), lose ? 'faint' : l.unknown ? 'warn' : l.role, on),
56
+ seg(fit(l.src, nw ? cols - 14 : 34).padEnd(nw ? 0 : 35), l.unknown ? 'warn' : lose ? 'muted' : 'value', on),
57
+ ...(nw ? [] : [seg(fit(l.scope, Math.max(0, cols - 50)), 'muted')])
58
+ ]);
59
+ }
60
+ for (const p of armed.slice(0, 4)) rows.push([seg(' · ', 'faint'), seg(fit(`"${p.rule_text}"`, cols - 24), 'value'), seg(` → ${p.policy?.effect || 'enforced'}`, 'indigo')]);
61
+ if (armed.length > 4) rows.push([seg(` … ${armed.length - 4} more armed`, 'muted')]);
62
+ if (staged.length) rows.push([seg(` ${plural(staged.length, 'draft')} not armed (enforce nothing until armed)`, 'muted')]);
63
+ let eff;
64
+ if (!pol.ok) eff = [seg(nw ? 'effective: unknown (policies unreadable)' : 'effective: unknown · the server\'s policies could not be read, so nothing here is called enforced', 'warn')];
65
+ else if (top?.tag === 'enforced') eff = [seg(nw ? 'effective: enforced (server wins)' : 'effective: enforced · server policy outranks pack and QUOX.md', 'indigo')];
66
+ else if (top) eff = [seg(nw ? `effective: ${top.tag} (advisory only)` : `effective: ${top.tag} · advisory, guides the agent but enforces nothing`, top.role === 'muted' ? 'value' : top.role)];
67
+ else eff = [seg('effective: none · no rules at any layer', 'muted')];
68
+ rows.push(eff);
69
+ rows.push([seg(nw ? '/rules files: the QUOX.md text' : '/rules files shows the QUOX.md text · /rules off|on', 'faint')]);
70
+ return { rows };
71
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * E6 memory facts with provenance, from GET /api/memory/search (memory service). Each fact shows what
3
+ * the record says about where it came from (provenance, type, source conversation or agent), how old
4
+ * and how trusted it is (confidence, verification, use), and any conflict state (superseded or
5
+ * invalid) in plain sight, never hidden.
6
+ */
7
+ import { seg, fit, ago, short, plural } from './model.js';
8
+
9
+ const PROVENANCE = {
10
+ model_inference: 'inferred by a model', user_stated: 'stated by a user', user_confirmed: 'confirmed by a user',
11
+ tool_output: 'from a tool result', document: 'from a document', imported: 'imported', system: 'system'
12
+ };
13
+ export const provenanceWord = (p) => (p ? PROVENANCE[p] || String(p).replace(/_/g, ' ') : null);
14
+
15
+ /** d: { query, results: [...], total?, now } */
16
+ export function memoryFacts(d, cols, { max = 5 } = {}) {
17
+ const now = d.now ?? Date.now();
18
+ const nw = cols < 70;
19
+ const inner = Math.max(20, cols - 6);
20
+ const rows = [];
21
+ const res = (d.results || []).slice(0, max);
22
+ if (!res.length) return { rows: [[seg(`No memory matches "${fit(d.query, 40)}".`, 'muted')]] };
23
+ rows.push([seg(`${plural((d.results || []).length, 'fact')} for "${fit(d.query, 40)}"${(d.results || []).length > max ? `, showing ${max}` : ''}`, 'muted')]);
24
+ const L = (label, text, role = 'value') => [seg(' '), seg(label.padEnd(7), 'muted'), seg(fit(text, inner - 9), role)];
25
+ for (const m of res) {
26
+ rows.push([seg(' ')]);
27
+ rows.push([seg('◆ ', 'indigo', true), seg(m.memory_role === 'fact' || !m.memory_role ? 'fact ' : `${m.memory_role} `, 'muted'), seg(fit(m.content, inner - 8), 'value', true)]);
28
+ const src = [provenanceWord(m.provenance), m.memory_type].filter(Boolean);
29
+ if (m.source_conversation_id) src.push(nw ? `chat ${short(m.source_conversation_id, 8)}` : `conversation ${short(m.source_conversation_id, 10)}`);
30
+ if (m.agent_id) src.push(`agent ${m.agent_id}`);
31
+ if (src.length) rows.push(L('source', src.join(' · ')));
32
+ const age = [];
33
+ const created = m.created_at || m.createdAt;
34
+ if (created) age.push(`${ago(created, now)} old`);
35
+ if (m.confidence != null) age.push(`confidence ${Math.round(Number(m.confidence) * 100)}%`);
36
+ if (m.verified_at) age.push(`verified${m.verified_by ? ` by ${short(m.verified_by, 10)}` : ''} ${ago(m.verified_at, now)} ago`);
37
+ else age.push('not verified');
38
+ if (m.access_count) age.push(`used ${plural(Number(m.access_count), 'time')}`);
39
+ rows.push(L('age', age.join(' · '), 'muted'));
40
+ if (m.superseded_by) rows.push(L('status', `superseded by ${short(m.superseded_by, 12)}`, 'warn'));
41
+ else if (m.valid === false || m.valid === 0) rows.push(L('status', 'marked invalid', 'error'));
42
+ }
43
+ return { rows };
44
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Inline object views (QUOXON plan E2/E3, lab group E and D): shared, pure helpers.
3
+ *
4
+ * A view is { box?, rows } where a row is an array of segments { text, role, bold? }. Roles are theme
5
+ * roles ('value', 'muted', 'ok', 'warn', 'error', 'key', 'sky', 'teal', 'indigo', 'violet', 'faint') or
6
+ * 'agent:<name>', 'provider:<id>', 'proto:<aee|aocl|volt|ward>'. Nothing here does I/O or reads a clock:
7
+ * time arrives as `now`. Rule for every builder: a field the server did not supply is omitted, never
8
+ * invented, and an unreachable source is said plainly.
9
+ */
10
+ import { KANA } from '../qhello.js';
11
+
12
+ export const seg = (text, role = 'value', bold = false) => ({ text: String(text ?? ''), role, bold });
13
+ export const len = (s) => [...String(s ?? '')].length;
14
+ export const rowText = (row) => row.map(s => s.text).join('');
15
+ export const rowLen = (row) => len(rowText(row));
16
+
17
+ /** Clip a string to n display chars with an ellipsis (the lab's fit()). */
18
+ export const fit = (s, n) => {
19
+ const g = [...String(s ?? '').replace(/\s+/g, ' ').trim()];
20
+ if (n <= 0) return '';
21
+ return g.length > n ? g.slice(0, Math.max(0, n - 1)).join('') + '…' : g.join('');
22
+ };
23
+
24
+ /** Clip a row of segments to n chars total; later segments shrink first, the last one gets the ellipsis. */
25
+ export function fitRow(row, n) {
26
+ const out = [];
27
+ let used = 0;
28
+ for (const s of row) {
29
+ const room = n - used;
30
+ if (room <= 0) break;
31
+ const l = len(s.text);
32
+ if (l <= room) { out.push(s); used += l; continue; }
33
+ out.push({ ...s, text: fit(s.text, room) });
34
+ used = n;
35
+ break;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /** Pad a row to exactly n chars (clip first). */
41
+ export function padRow(row, n) {
42
+ const r = fitRow(row, n);
43
+ const l = rowLen(r);
44
+ return l < n ? [...r, seg(' '.repeat(n - l), 'muted')] : r;
45
+ }
46
+
47
+ /** Server timestamps: epoch ms, ISO, or SQLite "YYYY-MM-DD HH:MM:SS" (UTC). */
48
+ export function parseTs(v) {
49
+ if (v == null || v === '') return null;
50
+ if (typeof v === 'number') return v;
51
+ const s = String(v);
52
+ const t = Date.parse(/^\d{4}-\d\d-\d\d \d\d:\d\d(:\d\d)?$/.test(s) ? s.replace(' ', 'T') + 'Z' : s);
53
+ return Number.isFinite(t) ? t : null;
54
+ }
55
+
56
+ /** "45s", "3m", "2h", "28d" between two epoch ms. '' when unknown. */
57
+ export function ago(ts, now) {
58
+ const t = parseTs(ts);
59
+ if (t == null || !Number.isFinite(now)) return '';
60
+ const s = Math.max(0, Math.floor((now - t) / 1000));
61
+ if (s < 60) return `${s}s`;
62
+ const m = Math.floor(s / 60);
63
+ if (m < 60) return `${m}m`;
64
+ const h = Math.floor(m / 60);
65
+ if (h < 48) return `${h}h`;
66
+ return `${Math.floor(h / 24)}d`;
67
+ }
68
+
69
+ /** Duration in ms -> "0.4s", "3.4s", "2m05". */
70
+ export function dur(ms) {
71
+ if (ms == null || !Number.isFinite(ms)) return '';
72
+ if (ms < 60000) return `${(Math.max(0, ms) / 1000).toFixed(1)}s`;
73
+ const s = Math.round(ms / 1000);
74
+ return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}`;
75
+ }
76
+
77
+ /** "21:20 UTC" from a timestamp (UTC on purpose: the box and the owner are in different zones). */
78
+ export function hhmm(ts) {
79
+ const t = parseTs(ts);
80
+ return t == null ? '' : `${new Date(t).toISOString().slice(11, 16)} UTC`;
81
+ }
82
+ export const day = (ts) => { const t = parseTs(ts); return t == null ? '' : new Date(t).toISOString().slice(0, 10); };
83
+ export const short = (id, n = 8) => { const s = String(id ?? ''); return s.length > n + 1 ? s.slice(0, n) + '…' : s; };
84
+ export const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
85
+
86
+ /** Box geometry the lab uses: the box is at most `max` wide and never wider than the terminal less 2. */
87
+ export const boxWidth = (cols, max) => Math.max(20, Math.min((cols || 80) - 2, max));
88
+
89
+ // ---- decode-in (the lab's text arrival) --------------------------------------------------------------
90
+ // Katakana flicker just ahead of the reveal edge; resolved letters keep their role. Pure: t and t0 in
91
+ // seconds, salt keeps rows distinct. The colour-only afterglow is left to the renderer (it maps `glow`).
92
+ const LEAD = 0.07, GLOW = 0.6, PER_CHAR = 0.012;
93
+ const hash = (a, b, c) => { let h = (a * 374761393 + b * 668265263 + c * 2147483647) | 0; h = Math.imul(h ^ (h >>> 13), 1274126177); return ((h ^ (h >>> 16)) >>> 0) / 4294967296; };
94
+
95
+ /**
96
+ * -> cells [{ ch, role, bold, glyph, glow }] for one row at time t (seconds since the object arrived).
97
+ * t = Infinity (or quiet) gives the settled row. A row starts at t0 and resolves left to right over dur.
98
+ */
99
+ export function decodeRow(row, t, t0 = 0, d = 0.25, salt = 1) {
100
+ const chars = [];
101
+ for (const s of row) for (const ch of s.text) chars.push({ ch, role: s.role, bold: Boolean(s.bold) });
102
+ if (!Number.isFinite(t)) return chars.map(c => ({ ...c, glyph: false, glow: 0 }));
103
+ const n = chars.length;
104
+ const span = Math.max(d, n * PER_CHAR);
105
+ return chars.map((c, i) => {
106
+ if (c.ch === ' ') return { ...c, glyph: false, glow: 0 };
107
+ const r = t0 + (n > 1 ? (i / (n - 1)) * span : 0), dt = t - r;
108
+ if (dt < -LEAD) return { ch: ' ', role: c.role, bold: false, glyph: false, glow: 0 };
109
+ if (dt < 0) return { ch: KANA[Math.floor(hash(i, Math.floor(t * 30), salt) * KANA.length)], role: 'sky', bold: false, glyph: true, glow: 0 };
110
+ return { ...c, bold: c.bold || dt < 0.05, glyph: false, glow: dt < GLOW ? (1 - dt / GLOW) ** 1.6 : 0 };
111
+ });
112
+ }
113
+
114
+ /** Seconds the arrival of a view takes: rows start 0.1 s apart and each resolves over 0.25 s. */
115
+ export function arrivalSeconds(view) {
116
+ const rows = view?.rows?.length || 0;
117
+ return rows ? 0.1 * (rows - 1) + Math.max(0.25, Math.max(...view.rows.map(r => rowLen(r))) * PER_CHAR) + GLOW : 0;
118
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * D1 evidence integrity chain and D3 reconciliation for one tool call of this session.
3
+ * AEE the call's envelope, read back (GET /aee/:envelope_id)
4
+ * AOCL aocl.* envelopes on the same correlation id (GET /aee/query?corr=)
5
+ * TOOL what the server recorded for the call (payload.success / tool_name) vs what the stream said
6
+ * WARD the envelope's inclusion receipt (POST /ward/verify-receipt, server-verified, bounded)
7
+ * A link is drawn confirmed only when its read succeeded and said so. Nothing is recomputed locally,
8
+ * so the card says the server's WARD verified it, never "recomputed locally".
9
+ *
10
+ * D3: a tool that ended effect_unknown is reconciled against the server's own record of the call
11
+ * (the ops.tool envelope's payload.success). No envelope id means it cannot be reconciled from here.
12
+ */
13
+ import { seg, fit, short, plural } from './model.js';
14
+
15
+ const LINKS = ['AEE', 'AOCL', 'TOOL', 'WARD'];
16
+
17
+ /** -> [{ name, ok: true|false|null, sub }] in chain order. ok null = not confirmed (unknown). */
18
+ export function chainLinks(d) {
19
+ const t = d.tool || {};
20
+ const env = d.envelope?.ok ? d.envelope.env : null;
21
+ const out = [];
22
+ out.push(env ? { name: 'AEE', ok: true, sub: `envelope ${short(env.id || t.envelopeId, 14)}` }
23
+ : { name: 'AEE', ok: d.envelope?.status === 404 ? false : null, sub: d.envelope?.status === 404 ? 'envelope not found' : `envelope not read${d.envelope?.error ? ` (${fit(d.envelope.error, 24)})` : ''}` });
24
+ const aocl = d.aocl?.ok ? d.aocl.list || [] : null;
25
+ if (aocl && aocl.length) {
26
+ const has = (i) => aocl.some(e => e.intent === i);
27
+ const pol = aocl.filter(e => e.intent === 'aocl.policy.evaluated');
28
+ const polOk = pol.length ? pol.every(e => e.payload?.allPassed !== false) : null;
29
+ const bits = [];
30
+ if (has('aocl.identity.verified')) bits.push('identity');
31
+ if (polOk != null) bits.push(polOk ? 'policy ok' : 'policy blocked');
32
+ out.push({ name: 'AOCL', ok: polOk === false ? false : true, sub: bits.length ? bits.join(' + ') : plural(aocl.length, 'AOCL record') });
33
+ } else out.push({ name: 'AOCL', ok: null, sub: aocl ? 'no AOCL record on this corr' : `not read${d.aocl?.error ? ` (${fit(d.aocl.error, 20)})` : ''}` });
34
+ const rec = env?.payload || null;
35
+ if (rec && rec.success != null) {
36
+ const want = t.status === 'ok';
37
+ const match = Boolean(rec.success) === want || t.status === 'effect_unknown';
38
+ out.push({ name: 'TOOL', ok: match && Boolean(rec.success), sub: `${rec.tool_name || t.tool || 'tool'} ${rec.success ? 'succeeded' : 'failed'}${match ? '' : ` (stream said ${t.status})`}` });
39
+ } else out.push({ name: 'TOOL', ok: null, sub: `${t.tool || 'tool'} ${t.status || ''} (no server record)`.trim() });
40
+ const r = d.receipt?.ok ? d.receipt.result : null;
41
+ if (r?.status === 'WITNESSED') out.push({ name: 'WARD', ok: true, sub: `witnessed seq ${r.entry?.seq}` });
42
+ else if (r?.status === 'UNTIPPED') out.push({ name: 'WARD', ok: null, sub: `seq ${r.entry?.seq}, no signed tip yet` });
43
+ else if (r?.status === 'NOT_WITNESSED') out.push({ name: 'WARD', ok: null, sub: 'not witnessed yet' });
44
+ else if (r?.status) out.push({ name: 'WARD', ok: false, sub: `${r.status}${r.reason ? ` ${r.reason}` : ''}` });
45
+ else out.push({ name: 'WARD', ok: null, sub: d.receipt?.timeout ? `no answer in ${d.receipt.timeout}s` : `not checked${d.receipt?.error ? ` (${fit(d.receipt.error, 24)})` : ''}` });
46
+ return out;
47
+ }
48
+
49
+ const markOf = (ok) => (ok === true ? ['✔', 'ok'] : ok === false ? ['✖', 'error'] : ['○', 'faint']);
50
+
51
+ /** d: { tool, envelope, aocl, receipt } -> view { rows } (wide row at >= 90 cols, vertical below). */
52
+ export function proofChain(d, cols) {
53
+ const links = chainLinks(d);
54
+ const rows = [];
55
+ rows.push([seg('proof ', 'muted'), seg(fit(`${d.tool?.tool || 'tool call'}${d.tool?.envelopeId ? ` · ${short(d.tool.envelopeId, 18)}` : ''}`, cols - 10), 'value', true)]);
56
+ if (cols >= 90) {
57
+ const head = [], sub = [], cw = Math.max(23, Math.floor((cols - 4) / 4));
58
+ links.forEach((l, i) => {
59
+ const role = `proto:${l.name.toLowerCase() === 'tool' ? 'volt' : l.name.toLowerCase()}`;
60
+ head.push(seg(l.name, l.name === 'TOOL' ? 'value' : role, true));
61
+ sub.push(seg(fit(l.sub, cw - 1).padEnd(i < 3 ? cw : 0), l.ok === false ? 'error' : 'muted'));
62
+ if (i < 3) {
63
+ const linked = l.ok === true && links[i + 1].ok === true;
64
+ head.push(seg(' ' + (linked ? '━' : '╌').repeat(cw - 2 - l.name.length) + ' ', linked ? 'indigo' : 'faint'));
65
+ }
66
+ });
67
+ rows.push(head, sub);
68
+ } else {
69
+ links.forEach((l, i) => {
70
+ const role = `proto:${l.name.toLowerCase() === 'tool' ? 'volt' : l.name.toLowerCase()}`;
71
+ const [m, mr] = markOf(l.ok);
72
+ rows.push([seg(m, mr), seg(' '), seg(l.name.padEnd(5), l.name === 'TOOL' ? 'value' : role, true), seg(fit(l.sub, cols - 12), l.ok === false ? 'error' : 'muted')]);
73
+ if (i < 3) rows.push([seg(l.ok === true && links[i + 1].ok === true ? '┃' : '╎', l.ok === true && links[i + 1].ok === true ? 'indigo' : 'faint')]);
74
+ });
75
+ }
76
+ const n = links.filter(l => l.ok === true).length;
77
+ const bad = links.filter(l => l.ok === false).length;
78
+ if (n === 4) rows.push([seg('✔ ', 'ok'), seg(cols < 60 ? 'chain verified · 4 links' : 'chain verified · 4 links · WARD receipt checked by the server', 'value')]);
79
+ else rows.push([seg(bad ? '✖ ' : '◐ ', bad ? 'error' : 'warn'), seg(`${n} of 4 links confirmed${bad ? ` · ${bad} contradicted` : ''}`, bad ? 'error' : 'warn')]);
80
+ return { rows, links };
81
+ }
82
+
83
+ /** One effect_unknown call reconciled against the server's record of it. */
84
+ export function reconcileItem(it) {
85
+ if (!it.envelopeId) return { mark: '?', role: 'muted', state: 'unverified', why: 'no envelope id, cannot be checked from here' };
86
+ if (!it.record) return { mark: '?', role: 'muted', state: 'unverified', why: it.error ? `record not read (${fit(it.error, 30)})` : 'not checked' };
87
+ if (!it.record.ok) return { mark: '?', role: 'muted', state: 'unverified', why: it.record.status === 404 ? 'no server record of the call' : `record not read (${fit(it.record.error || '', 30)})` };
88
+ const ok = it.record.env?.payload?.success;
89
+ if (ok === true) return { mark: '✔', role: 'ok', state: 'confirmed', why: 'server recorded success' };
90
+ if (ok === false) return { mark: '✖', role: 'error', state: 'mismatch', why: 'server recorded a failure, not done' };
91
+ return { mark: '?', role: 'muted', state: 'unverified', why: 'server record has no outcome' };
92
+ }
93
+
94
+ /** d: { items: [{ tool, envelopeId, record }] } -> view { rows } */
95
+ export function reconcileView(d, cols) {
96
+ const nw = cols < 60;
97
+ const items = d.items || [];
98
+ const rows = [[seg(nw ? `reconcile · ${items.length} unknown` : `reconcile · ${plural(items.length, 'call')} ended with effect unknown, checking each`, 'muted')]];
99
+ let conf = 0, mis = 0;
100
+ for (const it of items) {
101
+ const r = reconcileItem(it);
102
+ if (r.state === 'confirmed') conf++; else if (r.state === 'mismatch') mis++;
103
+ const row = [seg(r.mark, r.role), seg(' '), seg(fit(it.tool || 'tool', 15).padEnd(16), 'value'), seg(r.state.padEnd(12), r.role === 'muted' ? 'muted' : r.role, r.state !== 'unverified')];
104
+ if (!nw) row.push(seg(fit(r.why, cols - 34), 'muted'));
105
+ rows.push(row);
106
+ }
107
+ const un = items.length - conf - mis;
108
+ rows.push([seg(`${conf} confirmed · ${mis} mismatch · ${un} unverified`, mis ? 'warn' : 'muted')]);
109
+ return { rows };
110
+ }