quoxcli 0.4.0 → 0.6.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 (199) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/admin.js +7 -0
  6. package/lib/api/endpoints/agent-attach.js +7 -0
  7. package/lib/api/endpoints/compliance.js +25 -0
  8. package/lib/api/endpoints/governance.js +7 -0
  9. package/lib/api/endpoints/inbox.js +6 -0
  10. package/lib/api/endpoints/inboxq.js +58 -0
  11. package/lib/api/endpoints/index.js +4 -0
  12. package/lib/api/endpoints/lucid.js +17 -0
  13. package/lib/api/endpoints/mcp.js +35 -0
  14. package/lib/api/endpoints/mon.js +122 -0
  15. package/lib/api/endpoints/notesq.js +2 -0
  16. package/lib/api/endpoints/quoxplan.js +51 -0
  17. package/lib/api/endpoints/tools.js +13 -0
  18. package/lib/api/endpoints/v1.js +13 -0
  19. package/lib/commands/actions.js +51 -13
  20. package/lib/commands/admin.js +35 -0
  21. package/lib/commands/agentAttach.js +15 -0
  22. package/lib/commands/approvals.js +193 -3
  23. package/lib/commands/ask.js +1 -1
  24. package/lib/commands/auth.js +64 -11
  25. package/lib/commands/backup.js +11 -4
  26. package/lib/commands/capabilities.js +3 -2
  27. package/lib/commands/chat.js +15 -0
  28. package/lib/commands/claude.js +4 -1
  29. package/lib/commands/compliance.js +109 -0
  30. package/lib/commands/connect.js +69 -0
  31. package/lib/commands/contacts.js +2 -0
  32. package/lib/commands/dev.js +6 -8
  33. package/lib/commands/doctor.js +22 -3
  34. package/lib/commands/evidence.js +2 -1
  35. package/lib/commands/gripe.js +24 -13
  36. package/lib/commands/inbox.js +37 -3
  37. package/lib/commands/inboxq.js +147 -0
  38. package/lib/commands/kuma.js +78 -5
  39. package/lib/commands/licenceLinkView.js +311 -0
  40. package/lib/commands/linkedin.js +19 -0
  41. package/lib/commands/loop.js +30 -3
  42. package/lib/commands/lucid.js +7 -0
  43. package/lib/commands/lucidGoals.js +389 -0
  44. package/lib/commands/mail.js +427 -25
  45. package/lib/commands/mcp.js +190 -0
  46. package/lib/commands/memory.js +68 -4
  47. package/lib/commands/memorySync.js +158 -0
  48. package/lib/commands/mon.js +69 -144
  49. package/lib/commands/monitoring.js +68 -0
  50. package/lib/commands/notes.js +29 -7
  51. package/lib/commands/notesSeal.js +617 -0
  52. package/lib/commands/objective.js +5 -2
  53. package/lib/commands/offload.js +52 -12
  54. package/lib/commands/plan.js +487 -52
  55. package/lib/commands/provider.js +29 -8
  56. package/lib/commands/qlarity.js +32 -1
  57. package/lib/commands/run.js +2 -1
  58. package/lib/commands/server.js +37 -0
  59. package/lib/commands/setup.js +127 -75
  60. package/lib/commands/skills.js +4 -1
  61. package/lib/commands/slot.js +49 -9
  62. package/lib/commands/status.js +92 -44
  63. package/lib/commands/tool.js +30 -0
  64. package/lib/commands/vault.js +6 -3
  65. package/lib/commands/volt.js +6 -1
  66. package/lib/commands/walkthrough.js +14 -2
  67. package/lib/commands/watch.js +11 -2
  68. package/lib/commands/wazuh.js +880 -0
  69. package/lib/commands/wazuhIntake.js +358 -0
  70. package/lib/commands/wazuhReport.js +192 -0
  71. package/lib/commands/workflow.js +47 -4
  72. package/lib/commands/x.js +50 -0
  73. package/lib/core/announce.js +86 -0
  74. package/lib/core/auth.js +20 -2
  75. package/lib/core/callMe.js +118 -0
  76. package/lib/core/client.js +27 -1
  77. package/lib/core/commandRegistrars.js +9 -0
  78. package/lib/core/config.js +42 -3
  79. package/lib/core/confirm.js +68 -7
  80. package/lib/core/crossOrgApprovals.js +69 -0
  81. package/lib/core/errors.js +52 -1
  82. package/lib/core/governedSession.js +135 -5
  83. package/lib/core/inboxResolution.js +3 -2
  84. package/lib/core/loginGuard.js +73 -0
  85. package/lib/core/offload.js +120 -0
  86. package/lib/core/org-override.js +5 -1
  87. package/lib/core/output.js +30 -5
  88. package/lib/core/profileTrust.js +182 -0
  89. package/lib/core/quoxglyph.js +22 -1
  90. package/lib/core/router.js +14 -0
  91. package/lib/core/selfUpdate.js +2 -0
  92. package/lib/core/stepUpPrompt.js +99 -0
  93. package/lib/core/version.js +1 -1
  94. package/lib/core/walkthrough.js +9 -5
  95. package/lib/dragon/select.js +2 -2
  96. package/lib/quoxon/EXTENSIONS.md +142 -19
  97. package/lib/quoxon/api.js +189 -34
  98. package/lib/quoxon/app.js +1277 -188
  99. package/lib/quoxon/approvalBatch.js +117 -0
  100. package/lib/quoxon/approvalFacts.js +56 -2
  101. package/lib/quoxon/approvalSentence.js +8 -4
  102. package/lib/quoxon/approvalsView.js +72 -12
  103. package/lib/quoxon/ask.js +4 -2
  104. package/lib/quoxon/autoOpen.js +94 -0
  105. package/lib/quoxon/backgroundWork.js +59 -0
  106. package/lib/quoxon/bang.js +8 -2
  107. package/lib/quoxon/capabilities.js +7 -2
  108. package/lib/quoxon/commandShort.js +8 -1
  109. package/lib/quoxon/commands.js +42 -12
  110. package/lib/quoxon/connect.js +5 -3
  111. package/lib/quoxon/deckLazy.js +58 -0
  112. package/lib/quoxon/delegationDragon.js +49 -0
  113. package/lib/quoxon/destructive.js +35 -0
  114. package/lib/quoxon/dnd.js +118 -0
  115. package/lib/quoxon/dragonCompanion.js +37 -7
  116. package/lib/quoxon/dragonMoments.js +78 -0
  117. package/lib/quoxon/echo.js +1 -0
  118. package/lib/quoxon/events.js +55 -8
  119. package/lib/quoxon/extensions.js +400 -38
  120. package/lib/quoxon/foreignText.js +49 -0
  121. package/lib/quoxon/gripeQueue.js +135 -0
  122. package/lib/quoxon/inboxView.js +125 -22
  123. package/lib/quoxon/keys.js +2 -2
  124. package/lib/quoxon/launcher.js +2 -1
  125. package/lib/quoxon/loopCard.js +7 -2
  126. package/lib/quoxon/loopCards.js +17 -8
  127. package/lib/quoxon/loopStream.js +20 -9
  128. package/lib/quoxon/mailView.js +71 -0
  129. package/lib/quoxon/main.js +27 -6
  130. package/lib/quoxon/markdown.js +1 -1
  131. package/lib/quoxon/model.js +15 -0
  132. package/lib/quoxon/neverAnswered.js +20 -0
  133. package/lib/quoxon/objects/away.js +43 -5
  134. package/lib/quoxon/onboarding/connectServer.js +162 -0
  135. package/lib/quoxon/onboarding/copy.js +6 -0
  136. package/lib/quoxon/onboarding/dockerOffer.js +3 -1
  137. package/lib/quoxon/onboarding/index.js +5 -43
  138. package/lib/quoxon/onboarding/probe.js +20 -1
  139. package/lib/quoxon/onboarding/view.js +16 -3
  140. package/lib/quoxon/orgSwitch.js +22 -0
  141. package/lib/quoxon/print.js +2 -0
  142. package/lib/quoxon/profileSwitch.js +7 -0
  143. package/lib/quoxon/promptHistory.js +3 -1
  144. package/lib/quoxon/render.js +68 -16
  145. package/lib/quoxon/resend.js +14 -0
  146. package/lib/quoxon/roster.js +10 -5
  147. package/lib/quoxon/rosterSessions.js +44 -0
  148. package/lib/quoxon/rows.js +3 -1
  149. package/lib/quoxon/screenContext.js +1 -1
  150. package/lib/quoxon/secretShape.js +88 -0
  151. package/lib/quoxon/slash/agents.js +2 -1
  152. package/lib/quoxon/slash/argSources.js +9 -2
  153. package/lib/quoxon/slash/args.js +4 -2
  154. package/lib/quoxon/slash/broadcast.js +48 -11
  155. package/lib/quoxon/slash/broadcastRun.js +24 -7
  156. package/lib/quoxon/slash/cliLeaves.json +98 -1
  157. package/lib/quoxon/slash/dashboard.js +2 -0
  158. package/lib/quoxon/slash/hatWalk.js +1 -1
  159. package/lib/quoxon/slash/listView.js +2 -1
  160. package/lib/quoxon/slash/palette.js +63 -1
  161. package/lib/quoxon/slash/paletteController.js +15 -5
  162. package/lib/quoxon/slash/restart.js +9 -4
  163. package/lib/quoxon/slash/stepUp.js +5 -5
  164. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  165. package/lib/quoxon/slashSpinner.js +35 -0
  166. package/lib/quoxon/slotAttach.js +8 -0
  167. package/lib/quoxon/slotChat.js +24 -3
  168. package/lib/quoxon/slotFormat.js +1 -1
  169. package/lib/quoxon/slotOutbox.js +113 -0
  170. package/lib/quoxon/slotRun.js +2 -0
  171. package/lib/quoxon/suggestCommand.js +1 -1
  172. package/lib/quoxon/term.js +25 -1
  173. package/lib/quoxon/transcript.js +2 -1
  174. package/lib/quoxon/webPty.js +15 -0
  175. package/lib/quoxon/welcome.js +1 -1
  176. package/lib/quoxon/whatsnew.js +1 -1
  177. package/lib/quoxon/whatsnew.jsonl +42 -0
  178. package/lib/render/planBrief.js +614 -0
  179. package/lib/server/installFlow.js +168 -0
  180. package/lib/server/status.js +15 -1
  181. package/lib/toolOutputShape.js +61 -3
  182. package/package.json +3 -2
  183. package/vendor/dragonlab/SOURCE +3 -3
  184. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  185. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  186. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  187. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  188. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  189. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  190. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  191. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  192. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  193. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  194. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  195. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  196. package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
  197. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  198. package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
  199. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
@@ -5,6 +5,8 @@
5
5
  * dragon shows itself. The controller (dragonCompanion.js) runs the engine on every stream fact and owns the routing: while a
6
6
  * companion owns the moments the Q dragon stays quiet, so a moment is never two dragons.
7
7
  */
8
+ import { dragonForAgent } from '../dragon/select.js';
9
+
8
10
  export const MOMENTS = Object.freeze({
9
11
  approval: Object.freeze({ mood: 'waiting', companion: true }),
10
12
  failure: Object.freeze({ mood: 'failure', companion: true }),
@@ -80,3 +82,79 @@ export function createRosterWatch() {
80
82
  },
81
83
  };
82
84
  }
85
+
86
+ // ---- sender moments: a named session or agent speaks to the owner, so its own dragon appears (companion mode `on` only) ---------
87
+ /** The same sender cannot call its dragon again for this long once it has shown (a chatty session is one dragon, not a parade). */
88
+ export const SENDER_COOLDOWN_MS = 5 * 60 * 1000;
89
+ /** Only a message this recent is a moment: a backlog replayed on (re)connect is history, not news. */
90
+ export const SLOT_FRESH_MS = 2 * 60 * 1000;
91
+
92
+ const head = (s) => String(s).split(/[\s\-_.:/]+/)[0];
93
+ /** One name -> a dragon id: the whole name, its first word, or that word without trailing digits (quoxperticus-126). */
94
+ function dragonForName(name) {
95
+ const s = String(name ?? '').trim();
96
+ if (!s) return null;
97
+ return dragonForAgent(s) || dragonForAgent(head(s)) || dragonForAgent(head(s).replace(/\d+$/, ''));
98
+ }
99
+ /**
100
+ * A slot message sender -> its agent's dragon, or null. `from` is "slot (display)" or a bare name. The speaker (the display name
101
+ * in brackets) wins, then the bare name, then the slot it holds. Only approved dragons (select.js AGENT_DRAGON) can match.
102
+ */
103
+ export function dragonForSender(m) {
104
+ if (!m) return null;
105
+ const from = String(m.from ?? '').trim();
106
+ const inner = (from.match(/\(([^)]*)\)\s*$/) || [])[1];
107
+ const bare = from.replace(/\s*\(.*$/, '');
108
+ for (const n of [inner, bare, m.from_slot]) { const d = dragonForName(n); if (d) return d; }
109
+ return null;
110
+ }
111
+ const senderKey = (m) => String(m?.from_slot || m?.from || '').replace(/\s*\(.*$/, '').trim().toLowerCase();
112
+
113
+ /** A fresh slot message from a dragon session -> { dragon, sender, caption } | null. The caption says who and what kind, never the content. */
114
+ export function slotMessageMoment(m, { now = Date.now(), broadcast = false, run = false } = {}) {
115
+ const dragon = dragonForSender(m);
116
+ if (!dragon) return null;
117
+ const t = Date.parse(m.created_at ?? m.createdAt ?? '');
118
+ if (!Number.isFinite(t) || now - t > SLOT_FRESH_MS) return null;
119
+ const who = String(m.from).trim();
120
+ const caption = run ? `${who} asks you to run a command.` : broadcast ? `Broadcast from ${who}.` : `Message from ${who}.`;
121
+ return { dragon, sender: senderKey(m), caption };
122
+ }
123
+
124
+ /** Another session's approval card newly waiting, asked by an agent with a dragon -> { dragon, sender, key, caption } | null. */
125
+ export function waitingCardMoment(prev, next) {
126
+ const before = new Set((prev?.waiting || []).map(c => c.inboxItemId));
127
+ for (const c of next?.waiting || []) {
128
+ if (!c || before.has(c.inboxItemId)) continue;
129
+ const who = c.origin || c.askedBy;
130
+ const dragon = dragonForName(who) || dragonForName(c.askedBy);
131
+ if (!dragon) continue;
132
+ return { dragon, sender: String(who).toLowerCase(), key: c.inboxItemId, caption: `${who} wants your approval${c.tool ? ` (${c.tool})` : ''}.` };
133
+ }
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Dedupe + per-sender cooldown for sender moments. observe() remembers the message id at once (a re-delivery is never a second
139
+ * moment); shown() starts the sender's cooldown, so a moment refused because the owner was typing does not silence that sender.
140
+ */
141
+ export function createSenderWatch({ now = Date.now, cooldownMs = SENDER_COOLDOWN_MS } = {}) {
142
+ const seen = new Set(), last = new Map();
143
+ const remember = (k) => { seen.add(k); if (seen.size > 500) seen.delete(seen.values().next().value); };
144
+ const cool = (sender) => { const t = last.get(sender); return t != null && now() - t < cooldownMs; };
145
+ return {
146
+ observe(m, opts = {}) {
147
+ const id = m?.event_id || m?.id || (m?.created_at ? `${m.created_at}|${m.seq ?? ''}|${m.from ?? ''}` : null);
148
+ if (id != null) { if (seen.has(String(id))) return null; remember(String(id)); }
149
+ const mo = slotMessageMoment(m, { ...opts, now: now() });
150
+ return mo && !cool(mo.sender) ? mo : null;
151
+ },
152
+ card(prev, next) {
153
+ const mo = waitingCardMoment(prev, next);
154
+ if (!mo || seen.has(`card:${mo.key}`)) return null;
155
+ remember(`card:${mo.key}`);
156
+ return cool(mo.sender) ? null : mo;
157
+ },
158
+ shown(sender) { last.set(sender, now()); },
159
+ };
160
+ }
@@ -10,6 +10,7 @@ export const ECHO_CAP = 2000;
10
10
  export function echoLine(text, { cols = 100 } = {}) {
11
11
  let t = String(text ?? '').trim();
12
12
  if (PIN_CMD.test(t)) t = t.split(/\s+/)[0]; // a stray argument may be a PIN: never shown
13
+ if (/^\/verify-email(\s|$)/i.test(t) && /(^|\D)\d{6}(\D|$)/.test(t)) t = '/verify-email'; // a 6-digit run may be a PIN typed by mistake: never shown
13
14
  const full = redactSecrets(t).text;
14
15
  return { shown: clip(full.replace(/\s*\n\s*/g, ' '), ECHO_CAP), full };
15
16
  }
@@ -11,6 +11,9 @@
11
11
  * turn : null | { id, status: running|interrupting, startedAt, stage, activity }
12
12
  */
13
13
 
14
+ import { eventLine } from '../core/announce.js';
15
+ import { isDestructive, isDestructiveTool, destructiveObject } from './destructive.js';
16
+ import { heldReason } from './bulkRisk.js';
14
17
  import { approvalSentence } from './approvalSentence.js';
15
18
  import { dividerStep } from './dividerAocl.js';
16
19
  import { approvalFacts } from './approvalFacts.js';
@@ -80,6 +83,15 @@ export function textGate(st, ev) {
80
83
 
81
84
  const tms = (v) => { const t = typeof v === 'number' ? v : Date.parse(v); return Number.isFinite(t) ? t : null; };
82
85
 
86
+ /** A machine source key (`devsuite_pack_install:<host>:<pack>:<ver>`) is never shown as a requester: it names a human-readable origin instead. */
87
+ const SOURCE_KEY = /^[A-Za-z][\w.-]*(:[^:\s]+){2,}$/;
88
+ const SOURCE_NAMES = [[/^devsuite_/i, 'DevSuite (skill-pack sync)']];
89
+ export function humanRequester(v) {
90
+ const t = String(v ?? '').trim(); if (!t || !SOURCE_KEY.test(t)) return t || null;
91
+ const hit = SOURCE_NAMES.find(([re]) => re.test(t)); if (hit) return hit[1];
92
+ const head = t.split(':')[0].replace(/[_-]+/g, ' ').trim();
93
+ return head ? head.charAt(0).toUpperCase() + head.slice(1) : 'plugin or workflow';
94
+ }
83
95
  const short = (v, n = 120) => { const t = typeof v === 'string' ? v : (v == null ? '' : JSON.stringify(v)); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
84
96
  const argText = (a) => a && typeof a === 'object' && !Array.isArray(a) && (a.argv || a.command) ? argText(a.argv ?? a.command) : Array.isArray(a) ? a.join(' ') : (a && typeof a === 'object' ? Object.entries(a).filter(([, v]) => v != null && typeof v !== 'object').slice(0, 4).map(([k, v]) => `${k}=${v}`).join(' ') : (a == null ? '' : String(a)));
85
97
 
@@ -97,7 +109,7 @@ export function describeInboxItem(it) {
97
109
  const argv = m.argv ?? m.args ?? c.argv ?? c.args ?? p.argv ?? p.args ?? m.command ?? c.command ?? p.command ?? null;
98
110
  const cmd = short([m.leaf || c.leaf || p.leaf, argText(argv)].filter(Boolean).join(' ').trim() || null);
99
111
  const target = w.target || m.target || c.target || p.target || cmd || w.target_none || c.target_org || c.org_name || p.org_name || c.org_id || p.org_id || null;
100
- const by = ob.agent || ob.agent_id || ob.session_name || ob.session || c.requested_by?.agent_label || c.source_label || p.source || it?.source || null;
112
+ const by = humanRequester(ob.agent || ob.agent_id || ob.session_name || ob.session || c.requested_by?.agent_label || c.source_label || p.source || it?.source || null);
101
113
  const what = w.summary ? String(w.summary) : (it?.title ? short(String(it.title), 160) : null);
102
114
  return { tool, target: target ? short(target, 200) : null, title: what, by: by ? String(by) : null, at: it?.created_at ?? it?.createdAt ?? null };
103
115
  }
@@ -107,6 +119,13 @@ export function describeInboxItem(it) {
107
119
  * Qualifies when created after `since` (the turn start, else app start), or raised on behalf of this
108
120
  * user, or in this conversation. The inbox list is already scoped to the signed-in user and org.
109
121
  */
122
+ /** The contact-consent proposal on an inbox item (proposeOptin.js), or null. It has no executor: the TUI records the grant. */
123
+ export function consentOf(it) {
124
+ const p = it?.payload || {};
125
+ if (p.kind_detail !== 'contactability_optin' || !p.recipient || !p.channel) return null;
126
+ return { recipient: String(p.recipient), channel: String(p.channel), proposed: p.proposed && typeof p.proposed === 'object' ? p.proposed : {} };
127
+ }
128
+
110
129
  export function cardFromInboxItem(it, { since = 0, userId = null, conversationId = null, now = 0 } = {}) {
111
130
  if (!it || !it.id) return null;
112
131
  if (it.payload?.type === 'decision') return null; // quox ask: answered with /answer N key from /approvals, never a y/n key card
@@ -126,9 +145,13 @@ export function cardFromInboxItem(it, { since = 0, userId = null, conversationId
126
145
  const tool = d.tool || String(it.title || it.id);
127
146
  return {
128
147
  callId: `inbox:${it.id}`, inboxItemId: it.id, approvalId,
148
+ consent: consentOf(it), // a contact-consent proposal: approving records nothing until the person gives the lawful basis
129
149
  tool, target: d.target, what: d.title, askedBy: d.by, askedAt: d.at, scope: null, risk: c.risk || null,
130
150
  url: it.dashboard_url || null, shownAt: now, undescribable: !d.tool && !d.target && !it._plan,
131
151
  plan: it._plan ? { lines: planLines(it._plan.steps), loaded: it._plan.loaded !== false, orgId: it._plan.orgId || null, orgName: it._plan.orgName || null } : null, // the plan's steps + the item's own org
152
+ destructive: isDestructive(it) ? destructiveObject(it) : null, // typed-name confirm before a y approves it
153
+ disruptive: [it, it.payload, c, m, it.payload?.what].some(o => o && o.disruptive === true), // server-marked: never batched
154
+ held: heldReason(it), // a risky ask is never part of a batch card
132
155
  facts: approvalFacts(it), // APPROVAL-WWWH: the exact command, hosts, chain, reason, time and read-only class
133
156
  // only an item that names THIS conversation is ours; anything else came from another session and must not take the keyboard
134
157
  foreign: !here,
@@ -140,7 +163,7 @@ export function cardFromInboxItem(it, { since = 0, userId = null, conversationId
140
163
  /** Where an inbox approval came from: its source label, else the payload source, else its type. */
141
164
  export function approvalSourceLabel(it) {
142
165
  const c = it?.payload?.approval_context || {};
143
- return String(c.source_label || it?.payload?.source || it?.source || (c.requested_by && c.requested_by.agent_label) || it?.payload?.type || 'plugin or workflow');
166
+ return humanRequester(c.source_label || it?.payload?.source || it?.source || (c.requested_by && c.requested_by.agent_label) || it?.payload?.type || 'plugin or workflow');
144
167
  }
145
168
 
146
169
  /**
@@ -245,7 +268,7 @@ export function delegateInfo(ev) {
245
268
  if (ev.tool !== 'delegate_to_agent') return {};
246
269
  const id = ev.agent_id || (String(ev.summary || '').match(/\bagent_id=(\S+)/) || [])[1] || null;
247
270
  if (!id) return {};
248
- return { agentId: id, agentName: ev.agent_name || id, ...(ev.task_gist ? { taskGist: ev.task_gist } : {}) };
271
+ return { agentId: id, agentName: ev.agent_name || id, ...(ev.task_gist ? { taskGist: ev.task_gist } : {}), ...(ev.dragon === true ? { dragon: true } : {}) };
249
272
  }
250
273
 
251
274
  function upsertTool(s, ev, patch) {
@@ -377,7 +400,7 @@ function reduceInner(s, a) {
377
400
  case 'help':
378
401
  return { ...s, history: [...s.history, { id: nid(), kind: 'help', model: a.model }] };
379
402
  case 'object': // inline object view (objects/): pure data, rebuilt at render width
380
- return { ...s, history: [...s.history, { id: nid(), kind: 'object', obj: a.obj }] };
403
+ return { ...s, notify: scopeNotify(s.notify, a.obj), history: [...s.history, { id: nid(), kind: 'object', obj: a.obj }] };
381
404
  case 'error':
382
405
  return flush(addBlock(s, { kind: 'error', text: a.text }), false);
383
406
  case 'verbose':
@@ -435,11 +458,11 @@ function reduceInner(s, a) {
435
458
  const card = cardFromInboxItem(a.item, { since: s.turn?.startedAt ?? a.since ?? 0, userId: a.userId, conversationId: s.ctx.conversationId, now: a.now || 0 });
436
459
  if (card && card.approvalId) { // an event card raised without an inbox id learns it from the poll (else y answers a null id: 404)
437
460
  const j = s.approvals.findIndex(x => !x.inboxItemId && x.approvalId === card.approvalId);
438
- if (j >= 0) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], inboxItemId: card.inboxItemId, url: approvals[j].url || card.url, facts: approvals[j].facts || card.facts }; return { ...s, approvals, seenInbox: { ...s.seenInbox, [card.inboxItemId]: true } }; }
461
+ if (j >= 0) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], inboxItemId: card.inboxItemId, url: approvals[j].url || card.url, facts: approvals[j].facts || card.facts, destructive: approvals[j].destructive || card.destructive }; return { ...s, approvals, seenInbox: { ...s.seenInbox, [card.inboxItemId]: true } }; }
439
462
  }
440
463
  if (card && card.approvalId) { // an event stub that could not be described learns its description from the inbox item
441
464
  const j = s.approvals.findIndex(x => x.undescribable && (x.inboxItemId === card.inboxItemId || x.approvalId === card.approvalId));
442
- if (j >= 0 && !card.undescribable) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], tool: card.tool, target: card.target, what: card.what, askedBy: card.askedBy, askedAt: card.askedAt, facts: card.facts, undescribable: false }; return { ...s, approvals }; }
465
+ if (j >= 0 && !card.undescribable) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], tool: card.tool, target: card.target, what: card.what, askedBy: card.askedBy, askedAt: card.askedAt, facts: card.facts, destructive: approvals[j].destructive || card.destructive, undescribable: false }; return { ...s, approvals }; }
443
466
  }
444
467
  if (!card || s.seenInbox[card.inboxItemId] || s.approvals.some(x => x.inboxItemId === card.inboxItemId || (card.approvalId && x.approvalId === card.approvalId))) return s;
445
468
  if (card.foreign) { // another conversation's card: a compact notice, never the focused card
@@ -458,9 +481,15 @@ function reduceInner(s, a) {
458
481
  return { ...s, approvals: s.approvals.filter(x => x.callId !== a.callId) };
459
482
  case 'approval.sent': { // the human answered from this terminal
460
483
  const rest = { ...s, inbox: (s.inbox || []).filter(x => x.id !== a.inboxItemId), seenInbox: a.inboxItemId ? { ...s.seenInbox, [a.inboxItemId]: true } : s.seenInbox, approvals: s.approvals.filter(x => x.callId !== a.callId && !(a.inboxItemId && x.inboxItemId === a.inboxItemId)), waiting: s.waiting.filter(x => x.callId !== a.callId && x.inboxItemId !== a.callId) };
484
+ if (a.notRecorded) return note(rest, a.notRecorded, 'warn'); // the server answered 2xx but did not record the yes
485
+ if (a.consent && a.action === 'approve') return note(rest, `approved, but no consent is recorded yet: ${a.consent.recipient} via ${a.consent.channel} needs the lawful basis. Type it now (one line), or n to leave it unrecorded.`, 'warn');
461
486
  if (a.batch) return rest; // /approve N prints its own one-line summary
462
487
  if (a.quiet && a.action === 'approve') return rest;
463
488
  if (a.action !== 'approve') return note(rest, approvalSentence(a, false), 'warn');
489
+ if (a.foreign && a.approvalId) { // tracked: the poll adds one follow-up line with the outcome
490
+ const run = { callId: a.callId, approvalId: a.approvalId, tool: a.tool, target: a.target };
491
+ return note({ ...rest, running: [...rest.running.filter(x => x.callId !== a.callId), run] }, approvalSentence(a, true), 'ok');
492
+ }
464
493
  if (a.foreign) return note(rest, approvalSentence(a, true), 'ok');
465
494
  if (a.tool === 'hat.switch') { // applied server-side by the inbox approve; nothing to wait for
466
495
  const to = String(a.target || '').split('\u2192').pop().replace(/\(privileged\)/, '').trim() || 'the new hat';
@@ -475,8 +504,9 @@ function reduceInner(s, a) {
475
504
  const n = { ...s, running: s.running.filter(x => x.callId !== a.callId) };
476
505
  const what = run ? `${run.tool || 'approval'}${run.target ? ` on ${run.target}` : ''}` : 'approval';
477
506
  if (a.outcome === 'done') return note(n, `result: ${what}: ${a.text || 'completed'}`, 'ok');
478
- if (a.outcome === 'failed') return note(n, `failed: ${what}: ${a.text || 'error'}`, 'warn');
507
+ if (a.outcome === 'failed') return note(n, `${Number.isInteger(a.exit) && a.exit >= 1 ? `ran, exit ${a.exit}` : 'failed'}: ${what}: ${a.text || 'error'}`, 'warn');
479
508
  if (a.outcome === 'denied') return note(n, `denied: ${what}`, 'warn');
509
+ if (a.outcome === 'unrecorded') return note(n, `not run: ${what}. Your yes was saved in your inbox but never reached the tool runner: the server still shows the approval pending, with nobody recorded as deciding. Nothing ran. Ask the agent again for a fresh approval. Check: quox approvals result ${run && run.approvalId ? run.approvalId : '(no id)'}`, 'warn');
480
510
  // FW4: never silent and never a guess. Say what is unknown and where the authoritative answer is.
481
511
  const check = run && run.approvalId ? `quox approvals result ${run.approvalId} (or /approvals)` : '/approvals (no approval id was returned, so it cannot be tracked from here)';
482
512
  return note(n, `result not known yet: ${what}; approved, but the server has not said whether it ran. It may still be running or may have run. Check: ${check}`, 'warn');
@@ -495,6 +525,15 @@ function reduceInner(s, a) {
495
525
  }
496
526
 
497
527
  /** The first card is armed from the moment it is actually SHOWN, not from when it was received. */
528
+ /** An away/sitrep card that lists older items or a source it cannot see: the approvals bar must not say all-clear next to it. */
529
+ function scopeNotify(n, obj) {
530
+ const d = obj?.data;
531
+ if (!d || (obj.type !== 'away' && obj.type !== 'sitrep')) return n;
532
+ const outstanding = obj.type === 'away' ? (d.standing?.count || 0) : (d.needs || []).length;
533
+ const blind = Array.isArray(d.blind) ? d.blind.length : 0;
534
+ return { ...n, outstanding, blind };
535
+ }
536
+
498
537
  export function reduce(s, a) {
499
538
  let n = reduceInner(s, a);
500
539
  // TUI-DIVIDER: the bottom-rule AOCL strip, moved only by real turn events (dividerAocl.js).
@@ -593,7 +632,8 @@ function reduceEvent(s, ev, now) {
593
632
  target: ev.target, scope: ev.scope, risk: ev.risk, url: ev.dashboard_url, shownAt: now,
594
633
  undescribable: ev.describable === false || !ev.target,
595
634
  turnId: s.turn?.id ?? ev.turn_id ?? null, // the turn this card arrived in (a stream event is always this conversation)
596
- privileged: isPrivileged({ tool: ev.tool, risk: ev.risk, flag: ev.privileged === true || ev.elevated === true })
635
+ privileged: isPrivileged({ tool: ev.tool, risk: ev.risk, flag: ev.privileged === true || ev.elevated === true }),
636
+ destructive: ev.destructive === true || ev.irreversible === true || isDestructiveTool(ev.tool) ? (() => { const v = String(ev.object_name || ev.target || ev.inbox_item_id || ev.tool || 'item').slice(0, 80); return { label: v, name: v }; })() : null
597
637
  };
598
638
  if (s.approvals.some(x => x.callId === card.callId)) return s;
599
639
  const dup = s.approvals.findIndex(x => (card.inboxItemId && x.inboxItemId === card.inboxItemId) || (card.approvalId && x.approvalId === card.approvalId));
@@ -611,6 +651,13 @@ function reduceEvent(s, ev, now) {
611
651
  const n = { ...s, approvals: s.approvals.filter(x => x.callId !== ev.call_id) };
612
652
  return card || ev.decision ? note(n, `${ev.decision === 'approved' ? '✔ approved by you' : `approval ${ev.decision || 'resolved'}`}${card ? `: ${card.tool} on ${card.target}` : ''}`, ev.decision === 'approved' ? 'ok' : 'warn') : n;
613
653
  }
654
+ // QUOX-ANNOUNCE A2: a Quox effect of this turn (memory saved, approval held, delegation ...), one line, same renderer as the CLI verbs.
655
+ case 'quox_event': {
656
+ const text = eventLine(ev);
657
+ if (!text) return s;
658
+ if (ev.status === 'fail') return addBlock(s, { kind: 'error', text });
659
+ return addBlock(s, { kind: 'note', text, tone: ev.status === 'ok' ? 'ok' : ev.status === 'busy' ? 'accent' : 'warn' });
660
+ }
614
661
  case 'steer.accepted': {
615
662
  const i = s.steering.findIndex(x => x.text === ev.text);
616
663
  const steering = i >= 0 ? s.steering.filter((_, k) => k !== i) : s.steering.slice(1);