flowviant 0.125.30 → 0.125.33

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 (43) hide show
  1. package/bin/lib/agentPlan.mjs +8 -0
  2. package/bin/lib/agentQueueContext.mjs +6 -0
  3. package/bin/lib/agentSplit.mjs +90 -0
  4. package/bin/lib/architectureContract.mjs +57 -0
  5. package/bin/lib/architectureReport.mjs +38 -0
  6. package/bin/lib/artifactContracts.mjs +21 -2
  7. package/bin/lib/claudeInput.mjs +1 -1
  8. package/bin/lib/claudeSubagents.mjs +46 -6
  9. package/bin/lib/claudeWorkflows.mjs +94 -0
  10. package/bin/lib/cliLimit.mjs +5 -0
  11. package/bin/lib/codexInput.mjs +191 -0
  12. package/bin/lib/codexSubagentEvents.mjs +3 -1
  13. package/bin/lib/codexSubagents.mjs +34 -9
  14. package/bin/lib/conversationOutput.mjs +14 -0
  15. package/bin/lib/crossCliChild.mjs +327 -0
  16. package/bin/lib/crossCliLaunch.mjs +92 -0
  17. package/bin/lib/crossCliPosture.mjs +55 -0
  18. package/bin/lib/crossCliResearch.mjs +19 -0
  19. package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
  20. package/bin/lib/nativeAgentTools.mjs +12 -5
  21. package/bin/lib/orchestrator.mjs +24 -7
  22. package/bin/lib/orchestratorClient.mjs +8 -2
  23. package/bin/lib/orchestratorPrompt.mjs +13 -5
  24. package/bin/lib/orchestratorProtocol.mjs +18 -0
  25. package/bin/lib/outsideCli.mjs +124 -0
  26. package/bin/lib/prompts.mjs +7 -5
  27. package/bin/lib/runTurn.mjs +30 -8
  28. package/bin/lib/runtimeClaude.mjs +7 -3
  29. package/bin/lib/runtimeCodex.mjs +12 -6
  30. package/bin/lib/runtimeLimits.mjs +59 -1
  31. package/bin/lib/splitGate.mjs +115 -0
  32. package/bin/lib/subagentBounds.mjs +26 -0
  33. package/bin/lib/subagentLedger.mjs +110 -0
  34. package/bin/lib/trace.mjs +134 -6
  35. package/bin/lib/turnDeclaration.mjs +4 -4
  36. package/bin/lib/turnGaps.mjs +6 -2
  37. package/bin/lib/turnSubagents.mjs +23 -9
  38. package/bin/lib/wikiDraft.mjs +9 -0
  39. package/bin/lib/workAgentResume.mjs +11 -2
  40. package/bin/lib/workAgentTurnExecution.mjs +31 -7
  41. package/bin/lib/workAgentTurnOutcome.mjs +11 -0
  42. package/bin/lib/workAgentTurnReports.mjs +5 -2
  43. package/package.json +1 -1
@@ -0,0 +1,191 @@
1
+ /** Codex's native application protocol. Keep exec's config and event readers;
2
+ * only conversational build turns change transport. Fenced/structured jobs
3
+ * retain exec rather than translate a permission profile incompletely. */
4
+ import { randomUUID } from 'node:crypto';
5
+ import { versionAtLeast } from './versionOrder.mjs';
6
+
7
+ export function codexAppArgs(execArgs, version = null) {
8
+ if (version && !versionAtLeast(String(version).replace(/^codex-cli\s+/, ''), '0.166.0')) return null;
9
+ if (execArgs.includes('--ignore-user-config') || execArgs.includes('--output-schema')) return null;
10
+ const out = ['app-server', '--listen', 'stdio://'];
11
+ for (let i = 1; i < execArgs.length - 1; i++) {
12
+ const arg = execArgs[i];
13
+ if (arg === 'resume') {
14
+ if (execArgs[++i] === '--last') return null; // never guess another tab's thread
15
+ } else if (arg === '-c' || arg === '--config' || arg === '--enable' || arg === '--disable') {
16
+ out.push(arg, execArgs[++i]);
17
+ } else if (arg === '--model') out.push('-c', `model=${JSON.stringify(execArgs[++i])}`);
18
+ else if (arg === '--cd') i++;
19
+ else if (!['--json', '--skip-git-repo-check'].includes(arg)) return null;
20
+ }
21
+ return out;
22
+ }
23
+
24
+ const snake = s => s.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
25
+ export function codexAppItem(item) {
26
+ if (!item?.type) return null;
27
+ const out = Object.fromEntries(Object.entries(item).map(([key, value]) => [snake(key), value]));
28
+ out.type = item.type === 'collabAgentToolCall' ? 'collab_tool_call' : snake(item.type);
29
+ return out;
30
+ }
31
+
32
+ /** A response means the runtime accepted input, not that the model acted on
33
+ * it. clientUserMessageId is retained in native history for restart dedup.
34
+ * A completion racing a steer drains its RPC before closing stdin. */
35
+ export function createCodexInput(child, { cwd, threadId = null, onTaken = () => {}, onThread = () => {},
36
+ onEvent = () => {}, onFailure = () => {}, onContext = null, rpcTimeoutMs = 30_000 } = {}) {
37
+ let seq = 0, ready = false, active = null, starting = false, ended = false, broken = false;
38
+ let usage = null, previousUsage = null, lastContext = null;
39
+ const requests = new Map(), queued = [], sent = new Set(), seen = new Set(), completed = new Set();
40
+ const write = value => {
41
+ if (ended || broken || child.stdin.destroyed || child.stdin.writableEnded) return false;
42
+ try { child.stdin.write(JSON.stringify(value) + '\n'); return true; }
43
+ catch { broken = true; return false; }
44
+ };
45
+ const rpc = (method, params, done) => {
46
+ const id = ++seq;
47
+ const timer = setTimeout(() => {
48
+ requests.delete(id);
49
+ fail(`Codex did not acknowledge ${method} within ${rpcTimeoutMs / 1000} seconds. Unacknowledged input remains queued.`);
50
+ }, rpcTimeoutMs);
51
+ timer.unref?.();
52
+ requests.set(id, { method, done, timer });
53
+ if (!write({ id, method, params })) { clearTimeout(timer); requests.delete(id); return false; }
54
+ return true;
55
+ };
56
+ const end = () => { if (!ended) {
57
+ ended = true;
58
+ for (const request of requests.values()) clearTimeout(request.timer);
59
+ requests.clear(); child.stdin.end();
60
+ } };
61
+ const finishUsage = () => {
62
+ if (usage) onEvent({ type: 'turn.completed', usage });
63
+ usage = null;
64
+ };
65
+ const fail = message => {
66
+ if (ended) return;
67
+ finishUsage();
68
+ onFailure();
69
+ onEvent({ type: 'turn.failed', error: { message } });
70
+ end();
71
+ };
72
+ const inflight = () => [...requests.values()].some(r => r.method.startsWith('turn/'));
73
+ const pump = () => {
74
+ if (!ready || ended || broken || starting || inflight()) return;
75
+ while (queued.length && seen.has(queued[0].id)) onTaken(queued.shift().id);
76
+ const next = queued.shift();
77
+ if (!next) return;
78
+ const method = active ? 'turn/steer' : 'turn/start';
79
+ starting = !active;
80
+ const expected = active;
81
+ const params = { threadId, input: [{ type: 'text', text: next.prompt }], clientUserMessageId: next.id,
82
+ ...(active ? { expectedTurnId: active } : {}) };
83
+ if (!rpc(method, params, response => {
84
+ starting = false;
85
+ if (response.error) {
86
+ // The run can finish while this RPC is in the pipe. Only a measured
87
+ // completed turn permits a new native turn with the same input id.
88
+ if (method === 'turn/steer' && active === null) queued.unshift(next);
89
+ else if (method === 'turn/start') { fail(response.error.message || 'Codex could not start the turn.'); return; }
90
+ else onEvent({ type: 'error', message: response.error.message || 'Codex could not accept the follow-up; it remains queued.' });
91
+ } else {
92
+ seen.add(next.id);
93
+ onTaken(next.id);
94
+ if (method === 'turn/start' && response.result?.turn?.status === 'inProgress' && !completed.has(response.result.turn.id)) active = response.result.turn.id;
95
+ // A steer response must never resurrect its completed predecessor.
96
+ if (method === 'turn/steer' && active && active !== expected) return;
97
+ }
98
+ pump();
99
+ if (!active && !starting && !queued.length && !inflight()) end();
100
+ })) fail('Codex could not receive the complete turn input.');
101
+ };
102
+ child.stdin.on('error', () => { broken = true; onFailure(); });
103
+ return {
104
+ start() {
105
+ rpc('initialize', { clientInfo: { name: 'flowviant', title: 'Flowviant', version: '1' } }, response => {
106
+ if (response.error) { fail(response.error.message); return; }
107
+ write({ method: 'initialized', params: {} });
108
+ rpc(threadId ? 'thread/resume' : 'thread/start', { ...(threadId ? { threadId } : {}), cwd, approvalPolicy: 'never' }, response => {
109
+ if (response.error) { fail(response.error.message); return; }
110
+ const thread = response.result?.thread;
111
+ if (!thread?.id) { fail('Codex returned no conversation id.'); return; }
112
+ threadId = thread.id;
113
+ for (const turn of thread.turns ?? []) for (const item of turn.items ?? [])
114
+ if (item.type === 'userMessage' && item.clientId) seen.add(item.clientId);
115
+ onThread(threadId);
116
+ onEvent({ type: 'thread.started', thread_id: threadId });
117
+ ready = true;
118
+ // Recovery of the initial prompt: continue its existing work rather
119
+ // than repeat the original request already in the native transcript.
120
+ if (queued[0] && seen.has(queued[0].id)) {
121
+ const initial = queued.shift(); onTaken(initial.id);
122
+ queued.unshift({ id: `recovery-${randomUUID()}`, prompt: 'Continue the request already in this conversation. Inspect work in flight before repeating commands.' });
123
+ }
124
+ pump();
125
+ });
126
+ });
127
+ },
128
+ send(id, prompt) {
129
+ if (ended || broken || sent.has(id)) return false;
130
+ sent.add(id);
131
+ queued.push({ id, prompt });
132
+ pump();
133
+ return true;
134
+ },
135
+ event(event) {
136
+ if (ended) return;
137
+ if ('id' in event && !event.method) {
138
+ const request = requests.get(event.id);
139
+ if (request) { clearTimeout(request.timer); requests.delete(event.id); request.done(event); }
140
+ return;
141
+ }
142
+ const p = event.params ?? {};
143
+ // Server-originated approval/tool requests cannot hang an unattended
144
+ // process. Return a protocol refusal, never synthesize permission.
145
+ if ('id' in event && event.method) {
146
+ write({ id: event.id, error: { code: -32601, message: 'This unattended conversation cannot answer interactive requests.' } });
147
+ return;
148
+ }
149
+ if (p.threadId && p.threadId !== threadId) return;
150
+ if (event.method === 'turn/started') {
151
+ active = p.turn?.id ?? active;
152
+ onEvent({ type: 'turn.started' });
153
+ }
154
+ if (event.method === 'item/started' || event.method === 'item/completed') {
155
+ const item = codexAppItem(p.item);
156
+ if (item) onEvent({ type: event.method.replace('/', '.'), item });
157
+ }
158
+ if (event.method === 'thread/tokenUsage/updated') {
159
+ const last = p.tokenUsage?.last;
160
+ const fingerprint = JSON.stringify(p.tokenUsage?.total);
161
+ if (last && (active || starting) && fingerprint !== previousUsage) {
162
+ previousUsage = fingerprint;
163
+ usage ??= { input_tokens: 0, output_tokens: 0, cached_input_tokens: 0 };
164
+ usage.input_tokens += last.inputTokens ?? 0;
165
+ usage.output_tokens += last.outputTokens ?? 0;
166
+ usage.cached_input_tokens += last.cachedInputTokens ?? 0;
167
+ }
168
+ const window = p.tokenUsage?.modelContextWindow;
169
+ // Last request occupancy, never the thread's cumulative billable spend.
170
+ if (onContext && last && Number.isFinite(window) && window > 0) {
171
+ lastContext = { usedTokens: last.inputTokens + last.outputTokens, windowTokens: window, compactAt: null, compacting: false };
172
+ onContext(lastContext);
173
+ }
174
+ }
175
+ if (p.item?.type === 'contextCompaction' && lastContext && onContext && ['item/started', 'item/completed'].includes(event.method))
176
+ onContext({ ...lastContext, compacting: event.method === 'item/started' });
177
+ if (event.method === 'error' && p.willRetry !== true) fail(p.error?.message || 'Codex reported an error.');
178
+ if (event.method === 'turn/completed') {
179
+ if (active && p.turn?.id !== active) return;
180
+ completed.add(p.turn?.id);
181
+ active = null;
182
+ finishUsage();
183
+ if (p.turn?.status === 'failed') { fail(p.turn.error?.message || 'Codex failed the turn.'); return; }
184
+ if (p.turn?.status === 'interrupted') { fail('Codex interrupted the turn.'); return; }
185
+ pump();
186
+ if (!starting && !queued.length && !inflight()) end();
187
+ }
188
+ },
189
+ close() { ended = true; for (const request of requests.values()) clearTimeout(request.timer); requests.clear(); queued.length = 0; },
190
+ };
191
+ }
@@ -27,7 +27,9 @@ export function codexCollabUpdates(item) {
27
27
  const receiver = item.receiver_agents?.find(a => a.thread_id === id);
28
28
  const status = statusOf(state?.status ?? state);
29
29
  const message = state?.message ?? (typeof state?.completed === 'string' ? state.completed : null);
30
- return [{ id, ...(tool === 'spawn_agent' ? { ...(item.id ? { callId: item.id } : {}), ...(typeof item.prompt === 'string' ? { brief: item.prompt } : {}),
30
+ // The spawning thread is the child's parent; the reader names the lead's 'lead'.
31
+ const parent = tool === 'spawn_agent' && CODEX_CHILD_THREAD_RE.test(item.sender_thread_id ?? '') ? { parent: item.sender_thread_id } : {};
32
+ return [{ id, ...parent, ...(tool === 'spawn_agent' ? { ...(item.id ? { callId: item.id } : {}), ...(typeof item.prompt === 'string' ? { brief: item.prompt } : {}),
31
33
  ...(receiver?.agent_path || receiver?.agent_nickname || item.title ? { title: receiver?.agent_path ?? receiver?.agent_nickname ?? item.title } : {}),
32
34
  ...(receiver?.agent_role || item.agent_type ? { kind: receiver?.agent_role ?? item.agent_type } : {}),
33
35
  } : {}),
@@ -4,6 +4,14 @@
4
4
  * rust-v0.160.0: exec_events.rs CollabToolCallItem; core/agent/child_config.rs
5
5
  * reapplies the live turn's approval and permission profile after role config.
6
6
  * V2 rollout shapes measured on codex-cli 0.163.0 (codexSubagents.test.mjs).
7
+ *
8
+ * A child's own rollout says what it ran on (2026-11-06, S4.1): its newest
9
+ * `turn_context` names the model and effort that turn resolved, never what
10
+ * the spawn asked for; `session_meta`'s thread_spawn names the thread that
11
+ * spawned it (the lead, or another child: nesting); `task_complete` carries
12
+ * its final message, the result. `knownTargets` are a resumed agent's
13
+ * children from earlier turns (workAgentResume.mjs), name → thread, so a
14
+ * follow-up by task path reaches the record it continues.
7
15
  */
8
16
  import { open } from 'node:fs/promises';
9
17
  import { codexRolloutFile } from './runtimeLimits.mjs';
@@ -13,6 +21,9 @@ import { codexCollabUpdates, codexSpawnResult, statusOf } from './codexSubagentE
13
21
  import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
14
22
  import { isSealedMessage } from './orchestratorNative.mjs';
15
23
  import { SUBAGENT_CAP } from './subagentBounds.mjs';
24
+ import { turnModelOf } from './turnModel.mjs';
25
+
26
+ const EFFORT_RE = /^[a-z]{1,24}$/;
16
27
 
17
28
  const CODEX = NATIVE_AGENT_TOOLS.codex;
18
29
  // A rollout shows a superset of what the gate sees: send_message and the
@@ -22,7 +33,7 @@ const COLLAB_CALLS = [...CODEX.spawn, ...CODEX.followup, ...CODEX.message, ...CO
22
33
  const RECEIPT_CALLS = [...CODEX.spawn, ...CODEX.followup];
23
34
 
24
35
  export function createCodexSubagentReader({ since = Date.now(), env = process.env, onSubagent, onActivity, onToolEvent, onSpawnResult,
25
- findFile = codexRolloutFile } = {}) {
36
+ findFile = codexRolloutFile, knownTargets = null } = {}) {
26
37
  const readers = new Map();
27
38
  const calls = new Map();
28
39
  const targets = new Map();
@@ -33,11 +44,12 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
33
44
  let chain = Promise.resolve();
34
45
  const report = update => {
35
46
  if (!update?.id || !CODEX_CHILD_THREAD_RE.test(update.id)) return;
36
- // The root thread plus the run's children.
37
- if (!readers.has(update.id) && readers.size >= SUBAGENT_CAP + 1) return;
38
- if (!readers.has(update.id))
47
+ // The root thread plus the run's children are followed. A child past the
48
+ // cap is still reported, unfollowed: the relay counts it as not recorded.
49
+ if (!readers.has(update.id) && readers.size < SUBAGENT_CAP + 1)
39
50
  readers.set(update.id, { offset: null, rest: Buffer.alloc(0), seen: new Set(), live: false });
40
51
  const { message, ...record } = update;
52
+ if (record.parent && record.parent === root) record.parent = 'lead';
41
53
  onSubagent?.(record);
42
54
  };
43
55
  const rememberTarget = (path, id) => {
@@ -45,6 +57,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
45
57
  if (targets.size > 128) targets.delete(targets.keys().next().value);
46
58
  };
47
59
  const targetId = value => targets.get(value) ?? (CODEX_CHILD_THREAD_RE.test(value ?? '') ? value : null);
60
+ if (knownTargets) for (const [name, id] of knownTargets) if (CODEX_CHILD_THREAD_RE.test(id ?? '')) rememberTarget(name, id);
48
61
 
49
62
  function observe(ev, owner) {
50
63
  const at = Date.parse(ev?.timestamp ?? '');
@@ -65,10 +78,18 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
65
78
  if (src) {
66
79
  const title = src.agent_path ?? src.agent_nickname;
67
80
  if (src.agent_path) rememberTarget(src.agent_path, owner);
68
- report({ id: owner, ...(title ? { title } : {}), ...(src.agent_role ? { kind: src.agent_role } : {}) });
81
+ const parent = CODEX_CHILD_THREAD_RE.test(src.parent_thread_id ?? '') ? { parent: src.parent_thread_id } : {};
82
+ report({ id: owner, ...(title ? { title } : {}), ...(src.agent_role ? { kind: src.agent_role } : {}), ...parent });
69
83
  }
70
84
  return;
71
85
  }
86
+ if (ev.type === 'turn_context' && owner !== root) {
87
+ const model = turnModelOf(p.model);
88
+ const effort = p.effort ?? p.collaboration_mode?.settings?.reasoning_effort;
89
+ const brain = { ...(model ? { model } : {}), ...(typeof effort === 'string' && EFFORT_RE.test(effort) ? { effort } : {}) };
90
+ if (Object.keys(brain).length) report({ id: owner, ...brain });
91
+ return;
92
+ }
72
93
  if (ev.type === 'response_item' && p.type === 'function_call') {
73
94
  const name = String(p.name ?? '').split('.').at(-1);
74
95
  if (owner !== root && ['exec_command', 'shell_command', 'shell'].includes(name)) {
@@ -126,7 +147,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
126
147
  // A V2 message is ciphertext (orchestratorNative.mjs `isSealedMessage`):
127
148
  // its words are unknown, so the child carries no brief rather than that.
128
149
  const sealed = isSealedMessage(call.namespace ? `${call.namespace}.${call.name}` : call.name, args.message);
129
- report({ id, kind: args.agent_type ?? '', title, ...(sealed ? {} : { brief: args.message ?? '' }), status: 'running', now: '' });
150
+ report({ id, kind: args.agent_type ?? '', title, ...(sealed ? {} : { brief: args.message ?? '' }), status: 'running', now: '', parent: owner });
130
151
  return;
131
152
  }
132
153
  if (owner !== root && ev.type === 'response_item' && p.type === 'custom_tool_call') {
@@ -167,7 +188,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
167
188
  return;
168
189
  }
169
190
  if (p.type === 'collab_agent_spawn_end' && p.new_thread_id) {
170
- report({ id: p.new_thread_id, brief: p.prompt ?? '', kind: p.new_agent_role ?? '', title: p.new_agent_nickname ?? '',
191
+ report({ id: p.new_thread_id, brief: p.prompt ?? '', kind: p.new_agent_role ?? '', title: p.new_agent_nickname ?? '', parent: owner,
171
192
  status: statusOf(typeof p.status === 'object' ? Object.keys(p.status)[0]?.toLowerCase() : p.status) ?? 'running' });
172
193
  }
173
194
  if (owner === root) return;
@@ -181,7 +202,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
181
202
  onToolEvent?.('Write', { file_path: path, content: change?.add?.content ?? '' }, { agentId: owner, id: p.call_id });
182
203
  }
183
204
  if (p.type === 'task_started' || p.type === 'turn_started') report({ id: owner, status: 'running' });
184
- if (p.type === 'task_complete' || p.type === 'turn_complete') report({ id: owner, status: 'done', ...(p.last_agent_message ? { now: p.last_agent_message } : {}) });
205
+ if (p.type === 'task_complete' || p.type === 'turn_complete') report({ id: owner, status: 'done', ...(p.last_agent_message ? { now: p.last_agent_message, result: p.last_agent_message } : {}) });
185
206
  if (p.type === 'turn_aborted') report({ id: owner, status: 'stopped' });
186
207
  }
187
208
 
@@ -199,7 +220,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
199
220
  try {
200
221
  const ev = JSON.parse(line);
201
222
  if (initial && id !== root) initialEvents.push(ev);
202
- else if (id === root || state.live || ev.type === 'session_meta') observe(ev, id);
223
+ else if (id === root || state.live || ev.type === 'session_meta' || ev.type === 'turn_context') observe(ev, id);
203
224
  else if (ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type)) {
204
225
  state.live = true; observe(ev, id);
205
226
  }
@@ -210,6 +231,10 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
210
231
  // native turn belongs to this card; inherited history is never replayed.
211
232
  for (const ev of initialEvents) if (ev.type === 'session_meta') observe(ev, id);
212
233
  const start = initialEvents.findLastIndex(ev => ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type));
234
+ // The newest context is this turn's, wherever the CLI wrote it; an
235
+ // older turn's is refused by `since`.
236
+ const context = initialEvents.findLastIndex(ev => ev.type === 'turn_context');
237
+ if (context >= 0 && (start < 0 || context < start)) observe(initialEvents[context], id);
213
238
  if (start >= 0) {
214
239
  state.live = true;
215
240
  for (const ev of initialEvents.slice(start)) observe(ev, id);
@@ -0,0 +1,14 @@
1
+ /** One output vocabulary for Terminal, capture and agent conversations. */
2
+ export const CONVERSATION_OUTPUT = `CONVERSATION OUTPUT: both Terminal and agent chats render Markdown tables, highlighted code and LaTeX (use $$ delimiters for math; shell/console blocks use the reading font, source code uses monospace).
3
+ Printed source fences have Copy and Download; add file=script.py after the language to name the downloaded code file.
4
+ For a diagram, use a mermaid fence. For an illustration or an interactive view, use a flowviant-svg or flowviant-html fence. Those explicit fences render in an opaque sandbox: inline CSS/JS and data: images work; network requests, external scripts, forms, popups and access to the app do not. Ordinary html/svg/js fences remain printed source code. Keep inline visuals under 100,000 characters. Prefer retained artifacts for large, downloadable or reusable outputs.
5
+ For a native data card, use a flowviant-card fence containing JSON. Every card has type, title and optional description, source (absolute http/https URL), asOf. Supported shapes:
6
+ - weather, places, map, itinerary, products, comparison, options, links, sports, images, voting, dashboard, design-system, walkthrough: items [{title, description?, value?, detail?, url?}]. Map/image URLs are explicit links; no remote image loads on view.
7
+ - tasks: items [{title, description?, checked?}]. Checkboxes are local to the view, not changes to the project board.
8
+ - flashcards: items [{title, answer}].
9
+ - recipe: servings (1-100), ingredients [{name, quantity (number), unit?}], steps [string]. Servings scale ingredient quantities.
10
+ - quiz: questions [{question, options:[string], answer:zero-based-index, explanation?}].
11
+ - translation or email: body, optional original, to, subject. Copy is available; sending is never authorized by a card.
12
+ - chart: kind (line, bar or scatter), points [{label, y:number, x?:number}], optional xLabel and yLabel. Include numeric x for scatter plots.
13
+ Cards display supplied facts, never fetch live data: use your available tools for current facts, name the source and observation time, and retain uncertainty. Do not fabricate weather, places, prices, scores or voting information to fill a card. A card cannot authorize an action. Use the existing flowviant-ask format when the person must answer a question.
14
+ Kept documents, slides, spreadsheets, designs, whiteboards, trackers, dashboards, animations, design systems and web pages belong in .flowviant/artifacts/ using the supported artifact formats. Generate actual DOCX/PPTX/XLSX/PDF/CSV/image/video files when requested; the app previews supported formats and offers downloads. Do not claim an Office editor, external integration, live data lookup or file generation happened just because you printed a card.`;