@adia-ai/a2ui-mcp 0.6.4 → 0.6.5

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.
@@ -1,51 +1,35 @@
1
- /**
2
- * Synthesis tools chunk-based composition + multi-turn refinement.
3
- *
4
- * Extracts the 4 tools that share the LLM bridge + state-cache +
5
- * issue-reporter + chunk-refiner stack: `compose_from_chunks`,
6
- * `refine_composition`, `get_state`, `report_issue`.
7
- *
8
- * Spec: docs/specs/genui-multiturn-architecture.md (Phase A).
9
- */
10
-
11
- import { z } from 'zod';
12
-
13
- import { composeFromIntent as composeFromChunksImpl } from '../../compose/strategies/zettel/chunk-synthesizer.js';
14
- import { composeFromPlan, validatePlan } from '../../compose/strategies/zettel/chunk-composer.js';
15
- import { createAdapter as createLLMAdapter } from '../../../llm/llm-bridge.js';
1
+ import { z } from "zod";
2
+ import { composeFromIntent as composeFromChunksImpl } from "../../compose/strategies/zettel/chunk-synthesizer.js";
3
+ import { composeFromPlan, validatePlan } from "../../compose/strategies/zettel/chunk-composer.js";
4
+ import { createAdapter as createLLMAdapter } from "../../../llm/llm-bridge.js";
16
5
  import {
17
6
  getStateCache,
18
7
  mintStateId,
19
- mintNextStateId,
20
- } from '../../compose/strategies/zettel/state-cache.js';
8
+ mintNextStateId
9
+ } from "../../compose/strategies/zettel/state-cache.js";
21
10
  import {
22
11
  reportIssue as reportIssueImpl,
23
12
  autoReport,
24
- createIssueAccumulator,
25
- } from '../../compose/strategies/zettel/issue-reporter.js';
13
+ createIssueAccumulator
14
+ } from "../../compose/strategies/zettel/issue-reporter.js";
26
15
  import {
27
16
  refineFromIntent,
28
17
  applyOps,
29
18
  opsToA2UI,
30
- validateOps,
31
- } from '../../compose/strategies/zettel/chunk-refiner.js';
32
-
19
+ validateOps
20
+ } from "../../compose/strategies/zettel/chunk-refiner.js";
33
21
  const stateCache = getStateCache();
34
-
35
22
  const ENGINE_VERSION_INFO = {
36
- mcp: '0.2.0',
37
- corpus: '0.2.0',
38
- engine: 'zettel',
39
- llm_adapter: 'anthropic',
40
- model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-7',
23
+ mcp: "0.2.0",
24
+ corpus: "0.2.0",
25
+ engine: "zettel",
26
+ llm_adapter: "anthropic",
27
+ model: typeof process !== "undefined" && process.env["ANTHROPIC_MODEL"] || "claude-opus-4-7"
41
28
  };
42
-
43
- export { stateCache, ENGINE_VERSION_INFO, autoReport, reportIssueImpl };
44
-
45
- export function registerSynthesisTools(server) {
29
+ function registerSynthesisTools(server) {
46
30
  server.tool(
47
- 'compose_from_chunks',
48
- `Compose a UI page from training chunks retrieval-first, synthesis-fallback.
31
+ "compose_from_chunks",
32
+ `Compose a UI page from training chunks \u2014 retrieval-first, synthesis-fallback.
49
33
 
50
34
  Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflow:
51
35
  1. Pure-retrieval tier: if \`search_chunks\` returns a strong direct match, return
@@ -56,18 +40,18 @@ Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflo
56
40
 
57
41
  Returns the composed HTML string + a binding plan describing which chunks plug
58
42
  where. Useful when the prompt is novel ("dashboard with KPI grid + funnel +
59
- country list") and no exact chunk has all those parts together the LLM mixes
43
+ country list") and no exact chunk has all those parts together \u2014 the LLM mixes
60
44
  and matches from the corpus.
61
45
 
62
- Two-call mode also available via \`plan\` parameter pass a pre-baked binding
46
+ Two-call mode also available via \`plan\` parameter \u2014 pass a pre-baked binding
63
47
  plan to skip the LLM call and just materialize HTML.`,
64
48
  {
65
- intent: z.string().optional().describe('Natural-language description of what to build (uses LLM synthesis)'),
49
+ intent: z.string().optional().describe("Natural-language description of what to build (uses LLM synthesis)"),
66
50
  plan: z.object({
67
51
  page: z.string(),
68
- slot_bindings: z.record(z.union([z.string(), z.array(z.string())])),
69
- }).optional().describe('Pre-baked binding plan (skips LLM, materializes directly)'),
70
- max_attempts: z.number().int().min(1).max(5).default(2).describe('LLM retry budget for synthesis'),
52
+ slot_bindings: z.record(z.union([z.string(), z.array(z.string())]))
53
+ }).optional().describe("Pre-baked binding plan (skips LLM, materializes directly)"),
54
+ max_attempts: z.number().int().min(1).max(5).default(2).describe("LLM retry budget for synthesis")
71
55
  },
72
56
  async ({ intent, plan, max_attempts }) => {
73
57
  if (plan) {
@@ -75,45 +59,43 @@ plan to skip the LLM call and just materialize HTML.`,
75
59
  if (!validation.ok) {
76
60
  return {
77
61
  isError: true,
78
- content: [{ type: 'text', text: JSON.stringify({ error: 'invalid plan', errors: validation.errors }, null, 2) }],
62
+ content: [{ type: "text", text: JSON.stringify({ error: "invalid plan", errors: validation.errors }, null, 2) }]
79
63
  };
80
64
  }
81
65
  const result = composeFromPlan(plan);
82
- const state_id = mintStateId(intent || plan.page || 'plan', 1);
66
+ const state_id = mintStateId(intent ?? plan.page ?? "plan", 1);
83
67
  stateCache.set(state_id, {
84
68
  state_id,
85
- intent: intent || `(plan) ${plan.page}`,
69
+ intent: intent ?? `(plan) ${plan.page}`,
86
70
  plan: result.plan,
87
71
  html: result.html,
88
- source: 'plan',
72
+ source: "plan",
89
73
  ops_history: [],
90
74
  parent_state_id: null,
91
- created_at: new Date().toISOString(),
75
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
92
76
  });
93
77
  return {
94
- content: [{ type: 'text', text: JSON.stringify({
78
+ content: [{ type: "text", text: JSON.stringify({
95
79
  state_id,
96
80
  html: result.html,
97
81
  plan: result.plan,
98
82
  warnings: result.warnings,
99
- source: 'plan',
100
- }, null, 2) }],
83
+ source: "plan"
84
+ }, null, 2) }]
101
85
  };
102
86
  }
103
-
104
87
  if (!intent) {
105
88
  return {
106
89
  isError: true,
107
- content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or plan' }, null, 2) }],
90
+ content: [{ type: "text", text: JSON.stringify({ error: "must provide either intent or plan" }, null, 2) }]
108
91
  };
109
92
  }
110
-
111
93
  try {
112
94
  const llmAdapter = await createLLMAdapter();
113
95
  const result = await composeFromChunksImpl({
114
96
  intent,
115
97
  llmAdapter,
116
- maxAttempts: max_attempts,
98
+ maxAttempts: max_attempts
117
99
  });
118
100
  const state_id = mintStateId(intent, 1);
119
101
  stateCache.set(state_id, {
@@ -128,29 +110,23 @@ plan to skip the LLM call and just materialize HTML.`,
128
110
  warnings: result.warnings,
129
111
  synthesis: result.synthesis,
130
112
  scopeDrift: result.scopeDrift,
131
- created_at: new Date().toISOString(),
113
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
132
114
  });
133
-
134
- // Scope-drift auto-fire: composed HTML envelope exceeded the bound-
135
- // chunk envelope by > SCOPE_DRIFT_RATIO. Fires a `scope-drift` issue
136
- // (writes both .json and high-res .md) so post-mortem review can
137
- // catch the canvas-drift regression class without manual reporting.
138
115
  if (result.scopeDrift?.drift) {
139
116
  await autoReport(
140
- 'scope-drift',
117
+ "scope-drift",
141
118
  {
142
119
  intent,
143
120
  state_id,
144
121
  scopeDrift: result.scopeDrift,
145
- tags: ['canvas-drift'],
146
- trace: 'full',
122
+ tags: ["canvas-drift"],
123
+ trace: "full"
147
124
  },
148
125
  { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
149
126
  );
150
127
  }
151
-
152
128
  return {
153
- content: [{ type: 'text', text: JSON.stringify({
129
+ content: [{ type: "text", text: JSON.stringify({
154
130
  state_id,
155
131
  html: result.html,
156
132
  plan: result.plan,
@@ -158,180 +134,165 @@ plan to skip the LLM call and just materialize HTML.`,
158
134
  score: result.score,
159
135
  warnings: result.warnings,
160
136
  scopeDrift: result.scopeDrift,
161
- synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : undefined,
162
- }, null, 2) }],
137
+ synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : void 0
138
+ }, null, 2) }]
163
139
  };
164
140
  } catch (e) {
141
+ const err = e instanceof Error ? e : new Error(String(e));
165
142
  return {
166
143
  isError: true,
167
- content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
144
+ content: [{ type: "text", text: JSON.stringify({ error: err.message }, null, 2) }]
168
145
  };
169
146
  }
170
- },
147
+ }
171
148
  );
172
-
173
- // ── Multi-turn refinement (Phase A) ─────────────────────────────────
174
- // Spec: docs/specs/genui-multiturn-architecture.md §3.
175
-
176
149
  server.tool(
177
- 'refine_composition',
150
+ "refine_composition",
178
151
  `Refine an existing chunk-composed UI based on a natural-language intent or an explicit op-list.
179
152
 
180
153
  Use when the user wants to modify an *existing* UI. Triggers on "change", "update", "modify", "add to", "remove from", "this", "it", "the X". Requires \`state_id\` from a prior \`compose_from_chunks\` call.
181
154
 
182
155
  Two modes:
183
- - **Intent-driven** pass \`intent\`. Engine runs two-pass synthesis (locator pass identifies which slots to modify; modifier pass emits chunk-plan ops). Validator-driven retry on op-validation failure.
184
- - **Explicit ops** pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
156
+ - **Intent-driven** \u2014 pass \`intent\`. Engine runs two-pass synthesis (locator pass identifies which slots to modify; modifier pass emits chunk-plan ops). Validator-driven retry on op-validation failure.
157
+ - **Explicit ops** \u2014 pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
185
158
 
186
159
  Returns a new \`state_id\` (versioned chain from the parent), the A2UI op-list applied, the post-op HTML, and a delta summary. Failed ops are reported in \`ops_failed\` with reasons.
187
160
 
188
161
  For *fresh creation* use \`compose_from_chunks\`, not this tool.`,
189
162
  {
190
- state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
163
+ state_id: z.string().describe("State id from a prior compose_from_chunks or refine_composition call"),
191
164
  intent: z.string().optional().describe('Natural-language description of what to change (e.g. "add a country list to page-content")'),
192
- ops: z.array(z.any()).optional().describe('Pre-computed chunk-plan ops to apply directly (skips the LLM)'),
193
- max_attempts: z.number().int().min(1).max(5).default(2).describe('Validator retry budget for synthesis'),
165
+ ops: z.array(z.any()).optional().describe("Pre-computed chunk-plan ops to apply directly (skips the LLM)"),
166
+ max_attempts: z.number().int().min(1).max(5).default(2).describe("Validator retry budget for synthesis")
194
167
  },
195
168
  async ({ state_id, intent, ops, max_attempts }) => {
196
169
  const priorState = stateCache.get(state_id);
197
170
  if (!priorState) {
198
171
  await autoReport(
199
- 'cache-miss-on-known-state',
200
- { state_id, tool: 'refine_composition' },
172
+ "cache-miss-on-known-state",
173
+ { state_id, tool: "refine_composition" },
201
174
  { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
202
175
  );
203
176
  return {
204
177
  isError: true,
205
- content: [{ type: 'text', text: JSON.stringify({
206
- error: 'state_id not found in cache',
207
- hint: 'state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id',
208
- state_id,
209
- }, null, 2) }],
178
+ content: [{ type: "text", text: JSON.stringify({
179
+ error: "state_id not found in cache",
180
+ hint: "state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id",
181
+ state_id
182
+ }, null, 2) }]
210
183
  };
211
184
  }
212
-
213
185
  if (!intent && !ops) {
214
186
  return {
215
187
  isError: true,
216
- content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or ops' }, null, 2) }],
188
+ content: [{ type: "text", text: JSON.stringify({ error: "must provide either intent or ops" }, null, 2) }]
217
189
  };
218
190
  }
219
-
220
191
  const issueAccumulator = createIssueAccumulator();
221
192
  const issueCtx = { cache: stateCache, versionInfo: ENGINE_VERSION_INFO };
222
193
  const startedAt = Date.now();
223
-
224
194
  try {
225
195
  let resolvedOps;
226
- let delta_summary = '';
196
+ let delta_summary = "";
227
197
  let synthesis = null;
228
198
  let warnings = [];
229
-
230
199
  if (ops && Array.isArray(ops)) {
231
- // Explicit ops path — validate then apply
232
200
  const validation = validateOps(ops, priorState);
233
201
  if (!validation.ok) {
234
202
  await issueAccumulator.flush(issueCtx);
235
203
  return {
236
204
  isError: true,
237
- content: [{ type: 'text', text: JSON.stringify({
238
- error: 'ops failed validation',
239
- errors: validation.errors,
240
- }, null, 2) }],
205
+ content: [{ type: "text", text: JSON.stringify({
206
+ error: "ops failed validation",
207
+ errors: validation.errors
208
+ }, null, 2) }]
241
209
  };
242
210
  }
243
211
  resolvedOps = ops;
244
212
  delta_summary = `applied ${ops.length} explicit op(s)`;
245
213
  } else {
246
- // Intent path — two-pass synthesis with stub-friendly LLM bridge
247
214
  const llmAdapter = await createLLMAdapter();
248
215
  const refined = await refineFromIntent({
249
216
  priorState,
250
217
  intent,
251
218
  llmAdapter,
252
219
  maxAttempts: max_attempts,
253
- issueAccumulator,
220
+ issueAccumulator
254
221
  });
255
222
  resolvedOps = refined.ops;
256
- delta_summary = refined.delta_summary || '';
223
+ delta_summary = refined.delta_summary ?? "";
257
224
  synthesis = refined.synthesis;
258
225
  warnings = refined.warnings;
259
-
260
226
  if (resolvedOps.length === 0) {
261
- // Synthesizer gave up. Auto-fires already accumulated.
262
227
  await issueAccumulator.flush(issueCtx);
263
- const childId = mintNextStateId(state_id, (priorState.version || 1) + 1);
228
+ const childId = mintNextStateId(state_id, (priorState["version"] ?? 1) + 1);
229
+ const synthInfo2 = synthesis;
264
230
  return {
265
- content: [{ type: 'text', text: JSON.stringify({
231
+ content: [{ type: "text", text: JSON.stringify({
266
232
  state_id: childId,
267
233
  ops_applied: [],
268
234
  ops_failed: [],
269
- delta_summary: '',
235
+ delta_summary: "",
270
236
  warnings,
271
- synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted } : null,
272
- html: priorState.html,
273
- }, null, 2) }],
237
+ synthesis: synthInfo2 ? { attempts: synthInfo2["attempts"], targeted: synthInfo2["targeted"] } : null,
238
+ html: priorState["html"]
239
+ }, null, 2) }]
274
240
  };
275
241
  }
276
242
  }
277
-
278
243
  const applied = await applyOps({ priorState, ops: resolvedOps });
279
-
280
244
  if (applied.ops_failed.length > 0) {
281
- issueAccumulator.add('ops-failed-after-apply', {
245
+ issueAccumulator.add("ops-failed-after-apply", {
282
246
  state_id,
283
- tool: 'refine_composition',
284
- intent,
247
+ tool: "refine_composition",
248
+ intent
285
249
  });
286
250
  }
287
-
288
251
  const a2uiMessages = opsToA2UI(applied.ops_applied, applied.newState);
289
-
290
- const parentVersion = priorState.version || 1;
252
+ const parentVersion = priorState["version"] ?? 1;
291
253
  const newVersion = parentVersion + 1;
292
254
  const newStateId = mintNextStateId(state_id, newVersion);
293
-
294
255
  stateCache.set(newStateId, {
295
256
  state_id: newStateId,
296
- intent: intent || `(ops) ${priorState.intent}`,
297
- plan: applied.newState.plan,
298
- html: applied.newState.html,
299
- source: 'refinement',
257
+ intent: intent ?? `(ops) ${priorState["intent"] ?? ""}`,
258
+ plan: applied.newState["plan"],
259
+ html: applied.newState["html"],
260
+ source: "refinement",
300
261
  version: newVersion,
301
- ops_history: [...(priorState.ops_history || []), ...a2uiMessages],
262
+ ops_history: [...priorState["ops_history"] ?? [], ...a2uiMessages],
302
263
  parent_state_id: state_id,
303
- warnings: applied.newState.warnings,
264
+ warnings: applied.newState["warnings"],
304
265
  delta_summary,
305
266
  synthesis,
306
- created_at: new Date().toISOString(),
307
- duration_ms: Date.now() - startedAt,
267
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
268
+ duration_ms: Date.now() - startedAt
308
269
  });
309
-
310
270
  await issueAccumulator.flush(issueCtx);
311
-
271
+ const synthInfo = synthesis;
272
+ const newStateWarnings = applied.newState["warnings"] ?? [];
312
273
  return {
313
- content: [{ type: 'text', text: JSON.stringify({
274
+ content: [{ type: "text", text: JSON.stringify({
314
275
  state_id: newStateId,
315
276
  ops_applied: a2uiMessages,
316
277
  ops_failed: applied.ops_failed,
317
278
  delta_summary,
318
- warnings: [...warnings, ...(applied.newState.warnings || [])],
319
- synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted, locatedTargets: synthesis.locatedTargets } : null,
320
- html: applied.newState.html,
321
- }, null, 2) }],
279
+ warnings: [...warnings, ...newStateWarnings],
280
+ synthesis: synthInfo ? { attempts: synthInfo["attempts"], targeted: synthInfo["targeted"], locatedTargets: synthInfo["locatedTargets"] } : null,
281
+ html: applied.newState["html"]
282
+ }, null, 2) }]
322
283
  };
323
284
  } catch (e) {
324
285
  await issueAccumulator.flush(issueCtx);
286
+ const err = e instanceof Error ? e : new Error(String(e));
325
287
  return {
326
288
  isError: true,
327
- content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
289
+ content: [{ type: "text", text: JSON.stringify({ error: err.message }, null, 2) }]
328
290
  };
329
291
  }
330
- },
292
+ }
331
293
  );
332
-
333
294
  server.tool(
334
- 'get_state',
295
+ "get_state",
335
296
  `Inspect a cached composition state by state_id.
336
297
 
337
298
  Returns the full cache entry including the materialized HTML, the chunk binding plan, the chronological ops history (every refinement applied to this state's lineage), and the parent state_id (chain-back to the originating compose_from_chunks call).
@@ -340,51 +301,51 @@ Useful for debugging refinement sequences, replaying a state's history, or verif
340
301
 
341
302
  Auto-fires a low-severity \`cache-miss-on-known-state\` issue when the state_id is not in the cache (the cache is bounded LRU; long-paused conversations may evict their state).`,
342
303
  {
343
- state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
304
+ state_id: z.string().describe("State id from a prior compose_from_chunks or refine_composition call")
344
305
  },
345
306
  async ({ state_id }) => {
346
307
  const entry = stateCache.peek(state_id);
347
308
  if (!entry) {
348
309
  await autoReport(
349
- 'cache-miss-on-known-state',
350
- { state_id, tool: 'get_state' },
310
+ "cache-miss-on-known-state",
311
+ { state_id, tool: "get_state" },
351
312
  { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
352
313
  );
353
314
  return {
354
315
  isError: true,
355
- content: [{ type: 'text', text: JSON.stringify({
356
- error: 'state_id not found in cache',
357
- state_id,
358
- }, null, 2) }],
316
+ content: [{ type: "text", text: JSON.stringify({
317
+ error: "state_id not found in cache",
318
+ state_id
319
+ }, null, 2) }]
359
320
  };
360
321
  }
322
+ const e = entry;
361
323
  return {
362
- content: [{ type: 'text', text: JSON.stringify({
363
- state_id: entry.state_id,
364
- intent: entry.intent,
365
- plan: entry.plan,
366
- html: entry.html,
367
- source: entry.source,
368
- version: entry.version || 1,
369
- parent_state_id: entry.parent_state_id || null,
370
- ops_history: entry.ops_history || [],
371
- warnings: entry.warnings || [],
372
- created_at: entry.created_at,
373
- }, null, 2) }],
324
+ content: [{ type: "text", text: JSON.stringify({
325
+ state_id: e["state_id"],
326
+ intent: e["intent"],
327
+ plan: e["plan"],
328
+ html: e["html"],
329
+ source: e["source"],
330
+ version: e["version"] ?? 1,
331
+ parent_state_id: e["parent_state_id"] ?? null,
332
+ ops_history: e["ops_history"] ?? [],
333
+ warnings: e["warnings"] ?? [],
334
+ created_at: e["created_at"]
335
+ }, null, 2) }]
374
336
  };
375
- },
337
+ }
376
338
  );
377
-
378
339
  server.tool(
379
- 'report_issue',
380
- `File a structured issue ticket writes BOTH a machine-readable JSON file AND a human-readable Markdown report containing the full session trace (intent, retrieval log, LLM prompts, every attempt's raw response, composer plan, generated HTML preview, component count, warnings, environment).
340
+ "report_issue",
341
+ `File a structured issue ticket \u2014 writes BOTH a machine-readable JSON file AND a human-readable Markdown report containing the full session trace (intent, retrieval log, LLM prompts, every attempt's raw response, composer plan, generated HTML preview, component count, warnings, environment).
381
342
 
382
343
  When to call (any of these is a trigger):
383
- (a) USER PHRASES call immediately when the user says any of:
344
+ (a) USER PHRASES \u2014 call immediately when the user says any of:
384
345
  "file a ticket", "log a ticket", "save a ticket",
385
346
  "report this as a bug", "report this issue", "log this issue",
386
347
  "save the trace", "capture the session", "save the session for review",
387
- "create a session ticket", "this is broken debug it",
348
+ "create a session ticket", "this is broken \u2014 debug it",
388
349
  "download the trace", "export this for review",
389
350
  "track this regression", "open a ticket for this".
390
351
  (b) USER COMPLAINS the output is broken / wrong / missing.
@@ -397,20 +358,20 @@ Do NOT call this for ordinary clarification or for output the user has not yet s
397
358
 
398
359
  The tool returns BOTH paths in its response: \`path\` (.json) and \`markdown_path\` (.md). Surface BOTH to the user so they can navigate / download either:
399
360
 
400
- 📋 Logged ticket \`{issue_id}\` (\`{severity}\` · owner: {suggested_owner})
401
- Trace report: \`{markdown_path}\` human-readable, scan this first
402
- Raw JSON: \`{path}\` machine-readable
361
+ \u{1F4CB} Logged ticket \`{issue_id}\` (\`{severity}\` \xB7 owner: {suggested_owner})
362
+ \u2022 Trace report: \`{markdown_path}\` \u2190 human-readable, scan this first
363
+ \u2022 Raw JSON: \`{path}\` \u2190 machine-readable
403
364
 
404
365
  Issue files land under \`.brain/audit-history/issues/\` (immutable; resolution lands in a sidecar file). Severity taxonomy matches the project's ui-audit-coherence vocabulary: blocker = contract violation; drift = quality erosion; nit = cosmetic.`,
405
366
  {
406
- type: z.enum(['bug', 'training-gap', 'protocol-gap', 'ux-feedback']).describe('Issue category'),
407
- severity: z.enum(['blocker', 'drift', 'nit']).describe('Severity tier'),
408
- title: z.string().max(80).describe('One-line title ( 80 chars)'),
409
- body: z.string().describe('Markdown body observed vs expected, repro steps'),
410
- state_id: z.string().optional().describe('State id from a prior tool call; auto-attaches the trace'),
411
- trace: z.enum(['full', 'summary', 'none']).optional().describe('Trace depth DEFAULT: "full" when state_id is provided (writes both .json + .md ticket with retrieval log, LLM prompts/attempts, plan, HTML preview). Use "summary" for compact tickets; "none" to suppress trace entirely.'),
412
- suggested_owner: z.enum(['synthesis', 'retrieval', 'validator', 'chunk-corpus', 'mcp-protocol', 'unknown']).optional().describe('Best-guess owner for triage'),
413
- tags: z.array(z.string()).optional().describe('Free-form tags for filtering'),
367
+ type: z.enum(["bug", "training-gap", "protocol-gap", "ux-feedback"]).describe("Issue category"),
368
+ severity: z.enum(["blocker", "drift", "nit"]).describe("Severity tier"),
369
+ title: z.string().max(80).describe("One-line title (\u2264 80 chars)"),
370
+ body: z.string().describe("Markdown body \u2014 observed vs expected, repro steps"),
371
+ state_id: z.string().optional().describe("State id from a prior tool call; auto-attaches the trace"),
372
+ trace: z.enum(["full", "summary", "none"]).optional().describe('Trace depth \u2014 DEFAULT: "full" when state_id is provided (writes both .json + .md ticket with retrieval log, LLM prompts/attempts, plan, HTML preview). Use "summary" for compact tickets; "none" to suppress trace entirely.'),
373
+ suggested_owner: z.enum(["synthesis", "retrieval", "validator", "chunk-corpus", "mcp-protocol", "unknown"]).optional().describe("Best-guess owner for triage"),
374
+ tags: z.array(z.string()).optional().describe("Free-form tags for filtering")
414
375
  },
415
376
  async ({ type, severity, title, body, state_id, trace, suggested_owner, tags }) => {
416
377
  try {
@@ -419,18 +380,26 @@ Issue files land under \`.brain/audit-history/issues/\` (immutable; resolution l
419
380
  {
420
381
  cache: stateCache,
421
382
  versionInfo: ENGINE_VERSION_INFO,
422
- reporter: 'llm',
383
+ reporter: "llm"
423
384
  }
424
385
  );
425
386
  return {
426
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
387
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
427
388
  };
428
389
  } catch (e) {
390
+ const err = e instanceof Error ? e : new Error(String(e));
429
391
  return {
430
392
  isError: true,
431
- content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
393
+ content: [{ type: "text", text: JSON.stringify({ error: err.message }, null, 2) }]
432
394
  };
433
395
  }
434
- },
396
+ }
435
397
  );
436
398
  }
399
+ export {
400
+ ENGINE_VERSION_INFO,
401
+ autoReport,
402
+ registerSynthesisTools,
403
+ reportIssueImpl,
404
+ stateCache
405
+ };