@sales-bot-llm/sdk 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 (54) hide show
  1. package/biome.json +36 -0
  2. package/docs/superpowers/plans/2026-05-08-sales-bot-sdk-plan.md +258 -0
  3. package/docs/superpowers/plans/2026-05-11-w3-sales-tool-polish-plan.md +476 -0
  4. package/docs/superpowers/specs/2026-05-08-sales-bot-sdk-design.md +587 -0
  5. package/example/.env.example +5 -0
  6. package/example/README.md +90 -0
  7. package/example/index.html +12 -0
  8. package/example/package.json +27 -0
  9. package/example/public/vanilla.global.js +345 -0
  10. package/example/src/App.tsx +50 -0
  11. package/example/src/main.tsx +16 -0
  12. package/example/src/routes/HookDemo.tsx +174 -0
  13. package/example/src/routes/VanillaDemo.tsx +67 -0
  14. package/example/src/routes/WidgetDemo.tsx +55 -0
  15. package/example/src/styles.css +18 -0
  16. package/example/tsconfig.json +19 -0
  17. package/example/tsconfig.tsbuildinfo +1 -0
  18. package/example/vite.config.ts +4 -0
  19. package/package.json +106 -0
  20. package/pnpm-workspace.yaml +3 -0
  21. package/src/core/client.ts +245 -0
  22. package/src/core/conversation.ts +34 -0
  23. package/src/core/index.ts +6 -0
  24. package/src/core/sse-parser.ts +87 -0
  25. package/src/core/storage.ts +72 -0
  26. package/src/core/transport.ts +271 -0
  27. package/src/core/types.ts +314 -0
  28. package/src/core/visitor.ts +21 -0
  29. package/src/react/index.ts +2 -0
  30. package/src/react/use-sales-bot.tsx +182 -0
  31. package/src/vanilla/index.ts +38 -0
  32. package/src/vue/index.ts +2 -0
  33. package/src/vue/use-sales-bot.ts +152 -0
  34. package/src/widget/index.ts +3 -0
  35. package/src/widget/markdown.ts +69 -0
  36. package/src/widget/styles.ts +350 -0
  37. package/src/widget/widget.ts +442 -0
  38. package/tests/contract/wire-format.test.ts +158 -0
  39. package/tests/core/client.test.ts +292 -0
  40. package/tests/core/conversation.test.ts +41 -0
  41. package/tests/core/sse-parser.test.ts +142 -0
  42. package/tests/core/storage.test.ts +78 -0
  43. package/tests/core/transport.test.ts +204 -0
  44. package/tests/core/visitor.test.ts +42 -0
  45. package/tests/react/use-sales-bot.test.tsx +188 -0
  46. package/tests/sales-tool-discriminator.test.ts +45 -0
  47. package/tests/setup.ts +3 -0
  48. package/tests/vanilla/vanilla.test.ts +37 -0
  49. package/tests/vue/use-sales-bot.test.ts +163 -0
  50. package/tests/widget/markdown.test.ts +113 -0
  51. package/tests/widget/widget.test.ts +388 -0
  52. package/tsconfig.json +28 -0
  53. package/tsup.config.ts +38 -0
  54. package/vitest.config.ts +26 -0
@@ -0,0 +1,476 @@
1
+ # Workstream 3: SDK Sales-Tool Polish — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax.
4
+
5
+ **Goal:** Surface the new backend sales-workflow tool events as first-class SDK citizens — typed name discriminators, a hook callback for consumers, and friendly widget UI pills. Additive only; no breaking changes.
6
+
7
+ **Architecture:** Tiny additions to `src/core/types.ts` (literal union + type guard), `src/core/index.ts` (re-export), `src/react/use-sales-bot.tsx` and `src/vue/use-sales-bot.ts` (new optional `onSalesToolCall` callback), `src/widget/widget.ts` + `src/widget/styles.ts` (ephemeral status pill). One test file.
8
+
9
+ **Tech Stack:** TypeScript SDK (tsup build), Vitest, vanilla DOM widget.
10
+
11
+ **Design spec:** `/Users/artemzaitsev/projects/chaindoc/sales_bot/docs/superpowers/specs/2026-05-11-sales-workflow-dashboard-and-sdk-design.md` §6 + §3 rows 17-20.
12
+
13
+ ---
14
+
15
+ ## Hard constraints
16
+
17
+ 1. **Additive only — zero breaking changes.** Existing consumers continue to work without code changes.
18
+ 2. **No new dependencies.** No npm installs.
19
+ 3. **Existing tests stay green.**
20
+ 4. **Version bump to `0.2.0`** in `package.json` reflects non-breaking additions.
21
+ 5. **Wire types unchanged.** The new exports describe known tool *names*; they don't add new event types to `SalesBotEvent`.
22
+
23
+ ---
24
+
25
+ ## File map
26
+
27
+ | File | Action | Responsibility |
28
+ |---|---|---|
29
+ | `src/core/types.ts` | **Modify** | Add `SALES_WORKFLOW_TOOL_NAMES`, `SalesWorkflowToolName`, `isSalesWorkflowTool`. |
30
+ | `src/core/index.ts` | **Modify** | Already does `export * from './types'`, so the new exports flow through automatically. No edit needed if `*` re-export is in place. |
31
+ | `src/react/use-sales-bot.tsx` | **Modify** | Add `onSalesToolCall?: (e: ToolCallStartedEvent) => void` to `UseSalesBotOptions`; subscribe to `tool_call_started`, narrow on `isSalesWorkflowTool`, fire callback. |
32
+ | `src/vue/use-sales-bot.ts` | **Modify** | Same as the React hook. |
33
+ | `src/widget/widget.ts` | **Modify** | When a `tool_call_started` event matches a sales tool name, render an ephemeral status pill above the next assistant message. Remove on `tool_call_finished` with matching id. |
34
+ | `src/widget/styles.ts` | **Modify** | CSS for the status pill. |
35
+ | `tests/sales-tool-discriminator.spec.ts` | **Create** | Vitest unit tests for `isSalesWorkflowTool` + the React hook callback firing on the right events. |
36
+ | `package.json` | **Modify** | Bump version `0.1.0` → `0.2.0`. |
37
+
38
+ ---
39
+
40
+ ## Task 1: Baseline
41
+
42
+ - [ ] **Step 1.1:** Confirm baseline:
43
+
44
+ ```bash
45
+ pnpm install
46
+ pnpm test --run 2>&1 | tail -8
47
+ pnpm tsc --noEmit
48
+ ```
49
+
50
+ Expected: existing tests pass; tsc clean.
51
+
52
+ - [ ] **Step 1.2:** Note baseline test count. No commit.
53
+
54
+ ---
55
+
56
+ ## Task 2: Types + type guard + tests (RED → GREEN)
57
+
58
+ **Files:**
59
+ - Modify: `src/core/types.ts`
60
+ - Create: `tests/sales-tool-discriminator.spec.ts`
61
+
62
+ - [ ] **Step 2.1:** Write the failing test first.
63
+
64
+ ```typescript
65
+ // tests/sales-tool-discriminator.spec.ts
66
+ import { describe, it, expect } from 'vitest';
67
+ import {
68
+ SALES_WORKFLOW_TOOL_NAMES,
69
+ isSalesWorkflowTool,
70
+ type SalesWorkflowToolName,
71
+ } from '../src/core/types';
72
+
73
+ describe('SALES_WORKFLOW_TOOL_NAMES', () => {
74
+ it('is a readonly tuple containing all five sales-workflow tool names', () => {
75
+ expect(SALES_WORKFLOW_TOOL_NAMES).toEqual([
76
+ 'project_brief__set_field',
77
+ 'project_brief__get_current',
78
+ 'quotes__generate',
79
+ 'quotes__send_as_pdf',
80
+ 'quotes__send_as_proposal',
81
+ ]);
82
+ });
83
+ });
84
+
85
+ describe('isSalesWorkflowTool', () => {
86
+ it('returns true for each sales-workflow tool name', () => {
87
+ expect(isSalesWorkflowTool('project_brief__set_field')).toBe(true);
88
+ expect(isSalesWorkflowTool('project_brief__get_current')).toBe(true);
89
+ expect(isSalesWorkflowTool('quotes__generate')).toBe(true);
90
+ expect(isSalesWorkflowTool('quotes__send_as_pdf')).toBe(true);
91
+ expect(isSalesWorkflowTool('quotes__send_as_proposal')).toBe(true);
92
+ });
93
+
94
+ it('returns false for unrelated tool names', () => {
95
+ expect(isSalesWorkflowTool('chaindoc_media_upload')).toBe(false);
96
+ expect(isSalesWorkflowTool('llms_txt__search')).toBe(false);
97
+ expect(isSalesWorkflowTool('')).toBe(false);
98
+ expect(isSalesWorkflowTool('quotes__')).toBe(false);
99
+ expect(isSalesWorkflowTool('project_brief__unknown')).toBe(false);
100
+ });
101
+
102
+ it('narrows the type when used as a type guard', () => {
103
+ const name: string = 'quotes__generate';
104
+ if (isSalesWorkflowTool(name)) {
105
+ // Inside this branch, TypeScript narrows `name` to `SalesWorkflowToolName`.
106
+ const narrowed: SalesWorkflowToolName = name;
107
+ expect(narrowed).toBe('quotes__generate');
108
+ }
109
+ });
110
+ });
111
+ ```
112
+
113
+ - [ ] **Step 2.2:** Run the test — confirm it fails with "Cannot find module" or undefined-export.
114
+
115
+ ```bash
116
+ pnpm test --run tests/sales-tool-discriminator.spec.ts 2>&1 | tail -10
117
+ ```
118
+
119
+ Expected: red.
120
+
121
+ - [ ] **Step 2.3:** Add to `src/core/types.ts` — append at the bottom of the file (after the existing exports):
122
+
123
+ ```typescript
124
+ // ---------------------------------------------------------------------------
125
+ // Sales-workflow tool name discriminators
126
+ // ---------------------------------------------------------------------------
127
+ //
128
+ // The backend's SalesWorkflowDispatcher registers these tools with the LLM
129
+ // alongside MCP tools. They surface to the SDK via standard `tool_call_started`
130
+ // / `tool_call_finished` events with the names below. Consumers can narrow on
131
+ // these names via `isSalesWorkflowTool` for typed handling.
132
+
133
+ /** All sales-workflow tool names. Order is intentional (set_field/get_current
134
+ * first, quotes after). */
135
+ export const SALES_WORKFLOW_TOOL_NAMES = [
136
+ 'project_brief__set_field',
137
+ 'project_brief__get_current',
138
+ 'quotes__generate',
139
+ 'quotes__send_as_pdf',
140
+ 'quotes__send_as_proposal',
141
+ ] as const;
142
+
143
+ export type SalesWorkflowToolName = (typeof SALES_WORKFLOW_TOOL_NAMES)[number];
144
+
145
+ /** Type guard for narrowing tool-call event names to SalesWorkflowToolName. */
146
+ export function isSalesWorkflowTool(name: string): name is SalesWorkflowToolName {
147
+ return (SALES_WORKFLOW_TOOL_NAMES as readonly string[]).includes(name);
148
+ }
149
+ ```
150
+
151
+ - [ ] **Step 2.4:** Verify `src/core/index.ts` already does `export * from './types'`. If yes, no edit needed — the new exports flow through automatically. If it lists named re-exports instead, add the three names there.
152
+
153
+ - [ ] **Step 2.5:** Run the test — confirm 8/8 pass:
154
+
155
+ ```bash
156
+ pnpm test --run tests/sales-tool-discriminator.spec.ts 2>&1 | tail -10
157
+ ```
158
+
159
+ Expected: green.
160
+
161
+ - [ ] **Step 2.6:** `pnpm tsc --noEmit` — clean.
162
+
163
+ - [ ] **Step 2.7:** Commit:
164
+
165
+ ```bash
166
+ git add src/core/types.ts src/core/index.ts tests/sales-tool-discriminator.spec.ts
167
+ git commit -m "$(cat <<'EOF'
168
+ feat(core): SalesWorkflowToolName + isSalesWorkflowTool guard
169
+
170
+ Adds typed discriminators for the five sales-workflow tool events
171
+ the backend dispatches alongside MCP tools. Wire format is unchanged
172
+ — these names already arrive through the existing `tool_call_started`
173
+ / `tool_call_finished` events. Consumers can now narrow on them with
174
+ type safety:
175
+
176
+ client.on('tool_call_started', (e) => {
177
+ if (isSalesWorkflowTool(e.name)) {
178
+ // e.name is narrowed to SalesWorkflowToolName
179
+ }
180
+ });
181
+
182
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183
+ EOF
184
+ )"
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Task 3: React + Vue hook `onSalesToolCall` callback
190
+
191
+ **Files:**
192
+ - Modify: `src/react/use-sales-bot.tsx`
193
+ - Modify: `src/vue/use-sales-bot.ts`
194
+
195
+ - [ ] **Step 3.1:** React hook — read the existing `UseSalesBotOptions` definition, then extend it:
196
+
197
+ In `src/react/use-sales-bot.tsx`, find:
198
+
199
+ ```typescript
200
+ export interface UseSalesBotOptions extends SalesBotClientOptions {
201
+ /** Optional initial conversationId (e.g. restored from URL hash) */
202
+ conversationId?: string
203
+ }
204
+ ```
205
+
206
+ Add to this interface:
207
+
208
+ ```typescript
209
+ /**
210
+ * Fires when a sales-workflow tool call starts (e.g. quotes__send_as_pdf).
211
+ * Convenience over manually filtering tool_call_started events via
212
+ * isSalesWorkflowTool. Pass-through; receives the same payload.
213
+ */
214
+ onSalesToolCall?: (event: import('../core/types').ToolCallStartedEvent) => void
215
+ ```
216
+
217
+ Inside the hook body, after the client is constructed and the event subscription block, wire up the callback. Find the existing `tool_call_started` handler (if any) or the `client.on(...)` subscription pattern. If the hook currently subscribes to events in a `useEffect`, add a sibling subscription:
218
+
219
+ ```typescript
220
+ useEffect(() => {
221
+ if (!opts.onSalesToolCall) return;
222
+ const client = clientRef.current;
223
+ if (!client) return;
224
+ const handler = (e: import('../core/types').ToolCallStartedEvent) => {
225
+ // Lazy-load isSalesWorkflowTool from the same module to avoid widening
226
+ // the runtime cost when consumers don't use this callback.
227
+ const { isSalesWorkflowTool } = require('../core/types') as typeof import('../core/types');
228
+ if (isSalesWorkflowTool(e.name)) {
229
+ opts.onSalesToolCall!(e);
230
+ }
231
+ };
232
+ const off = client.on('tool_call_started', handler);
233
+ return () => { if (typeof off === 'function') off(); };
234
+ }, [opts.onSalesToolCall]);
235
+ ```
236
+
237
+ If the existing hook subscribes via `client.on(...)` returning an unsubscribe function, use that. If it uses async iteration over `client.events()` or similar, adapt this to match the existing subscription pattern. Read the actual file before writing the snippet — the comment above is the intent, not the literal final code.
238
+
239
+ `import('../core/types').ToolCallStartedEvent` is used in-line (no top-level type import) to keep the existing imports list minimal — change to a top-of-file import if the file's style prefers that.
240
+
241
+ - [ ] **Step 3.2:** Vue hook — same change in `src/vue/use-sales-bot.ts`. The Vue composable structure differs (uses Vue refs / watchers); follow the existing event-subscription pattern in that file.
242
+
243
+ - [ ] **Step 3.3:** Type-check:
244
+
245
+ ```bash
246
+ pnpm tsc --noEmit
247
+ ```
248
+
249
+ Expected: clean.
250
+
251
+ - [ ] **Step 3.4:** Run the existing test suite — confirm nothing regressed:
252
+
253
+ ```bash
254
+ pnpm test --run 2>&1 | tail -8
255
+ ```
256
+
257
+ Expected: all pass.
258
+
259
+ - [ ] **Step 3.5:** Commit:
260
+
261
+ ```bash
262
+ git add src/react/ src/vue/
263
+ git commit -m "$(cat <<'EOF'
264
+ feat(hooks): onSalesToolCall callback in React + Vue hooks
265
+
266
+ Optional hook option that fires for sales-workflow tool calls
267
+ (quotes__*, project_brief__*) only. Convenience over manually
268
+ filtering tool_call_started events with isSalesWorkflowTool.
269
+ Additive — existing consumers continue to work unchanged.
270
+
271
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
272
+ EOF
273
+ )"
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Task 4: Widget UI pills
279
+
280
+ **Files:**
281
+ - Modify: `src/widget/widget.ts`
282
+ - Modify: `src/widget/styles.ts`
283
+
284
+ - [ ] **Step 4.1:** Pill text mapping. In `src/widget/widget.ts`, near the top (after imports), add:
285
+
286
+ ```typescript
287
+ import { isSalesWorkflowTool, type SalesWorkflowToolName } from '../core/types';
288
+
289
+ const SALES_TOOL_PILL_TEXT: Record<SalesWorkflowToolName, string> = {
290
+ 'project_brief__set_field': 'Saving project details…',
291
+ 'project_brief__get_current': 'Checking project details…',
292
+ 'quotes__generate': 'Generating quote…',
293
+ 'quotes__send_as_pdf': 'Sending quote PDF…',
294
+ 'quotes__send_as_proposal': 'Sending proposal for signing…',
295
+ };
296
+ ```
297
+
298
+ - [ ] **Step 4.2:** Find the existing `tool_call_started` / `tool_call_finished` handlers in the widget. They currently call `ensureThinking()` etc. Augment them so:
299
+
300
+ - On `tool_call_started` with a sales tool name: also create a pill DOM element with the matching text, append to the current message-stream container, and track it by the event's `id`.
301
+ - On `tool_call_finished` with a sales tool name (matching id): remove the pill. If `ok: false`, briefly (4 seconds) replace the pill text with "Error" before removing.
302
+
303
+ Suggested implementation — adapt to match the existing widget file's idioms:
304
+
305
+ ```typescript
306
+ // State at the module / instance scope (where other widget state lives):
307
+ const salesPills = new Map<string, HTMLElement>();
308
+
309
+ // Inside the tool_call_started handler:
310
+ case 'tool_call_started': {
311
+ ensureThinking();
312
+ if (isSalesWorkflowTool(event.data.name)) {
313
+ const pill = document.createElement('div');
314
+ pill.className = 'sb-sales-pill';
315
+ pill.textContent = SALES_TOOL_PILL_TEXT[event.data.name as SalesWorkflowToolName];
316
+ // Append to whatever container the widget uses for the active message stream.
317
+ // If you keep a reference to the "current assistant message" container, append there;
318
+ // otherwise append to a known stream root.
319
+ streamContainer.appendChild(pill);
320
+ salesPills.set(event.data.id, pill);
321
+ }
322
+ break;
323
+ }
324
+
325
+ case 'tool_call_finished': {
326
+ const pill = salesPills.get(event.data.id);
327
+ if (pill) {
328
+ if (event.data.ok === false) {
329
+ pill.textContent = 'Error';
330
+ pill.classList.add('sb-sales-pill-error');
331
+ setTimeout(() => {
332
+ pill.remove();
333
+ salesPills.delete(event.data.id);
334
+ }, 4000);
335
+ } else {
336
+ pill.remove();
337
+ salesPills.delete(event.data.id);
338
+ }
339
+ }
340
+ // existing tool_call_finished handling continues:
341
+ // hideThinkingIfDone(); etc.
342
+ break;
343
+ }
344
+ ```
345
+
346
+ Read the current widget file before applying this; the `streamContainer` reference and the `ensureThinking()` / `hideThinkingIfDone()` helpers must come from the existing widget code.
347
+
348
+ - [ ] **Step 4.3:** Append pill styles in `src/widget/styles.ts`. The widget styles are CSS-in-template-strings or similar; add a block:
349
+
350
+ ```css
351
+ .sb-sales-pill {
352
+ display: inline-flex;
353
+ align-items: center;
354
+ margin: 4px 0;
355
+ padding: 4px 10px;
356
+ border-radius: 999px;
357
+ background: var(--sb-pill-bg, rgba(0, 0, 0, 0.05));
358
+ color: var(--sb-pill-fg, rgba(0, 0, 0, 0.7));
359
+ font-size: 12px;
360
+ line-height: 1.3;
361
+ animation: sb-sales-pill-pulse 1.8s ease-in-out infinite;
362
+ }
363
+ .sb-sales-pill-error {
364
+ background: var(--sb-pill-error-bg, rgba(255, 69, 58, 0.12));
365
+ color: var(--sb-pill-error-fg, rgba(155, 28, 28, 0.95));
366
+ animation: none;
367
+ }
368
+ @keyframes sb-sales-pill-pulse {
369
+ 0%, 100% { opacity: 1; }
370
+ 50% { opacity: 0.55; }
371
+ }
372
+ ```
373
+
374
+ If `styles.ts` exports a single template string of CSS, append the rules into that string. If it exports a function that returns CSS, add inside the function body. Match the file's actual structure.
375
+
376
+ - [ ] **Step 4.4:** Manually verify the widget compiles and renders (open `example/` against a local backend if available — optional). At minimum:
377
+
378
+ ```bash
379
+ pnpm tsc --noEmit
380
+ pnpm test --run 2>&1 | tail -8
381
+ ```
382
+
383
+ Expected: clean + green.
384
+
385
+ - [ ] **Step 4.5:** Commit:
386
+
387
+ ```bash
388
+ git add src/widget/
389
+ git commit -m "$(cat <<'EOF'
390
+ feat(widget): ephemeral status pills for sales-workflow tool calls
391
+
392
+ When the widget receives tool_call_started for project_brief__* or
393
+ quotes__* events, it renders a small pulsing pill above the active
394
+ assistant-message stream with friendly text ("Generating quote…",
395
+ "Sending proposal for signing…", etc.). On tool_call_finished the
396
+ pill is removed; on failure (ok=false) it briefly switches to an
397
+ "Error" state for 4 seconds before vanishing.
398
+
399
+ No-op for backends that don't dispatch these tool names — older
400
+ SDK behavior is preserved.
401
+
402
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
403
+ EOF
404
+ )"
405
+ ```
406
+
407
+ ---
408
+
409
+ ## Task 5: Version bump + final verification
410
+
411
+ **Files:**
412
+ - Modify: `package.json`
413
+
414
+ - [ ] **Step 5.1:** Bump version in `package.json` from `0.1.0` to `0.2.0`:
415
+
416
+ ```bash
417
+ # Edit package.json:
418
+ # "version": "0.1.0" → "version": "0.2.0"
419
+ ```
420
+
421
+ - [ ] **Step 5.2:** Run the build to confirm the SDK still bundles cleanly:
422
+
423
+ ```bash
424
+ pnpm build 2>&1 | tail -15
425
+ ```
426
+
427
+ Expected: tsup outputs `dist/` files without errors. Size-limit checks (if present) should still pass.
428
+
429
+ - [ ] **Step 5.3:** Final tests + tsc:
430
+
431
+ ```bash
432
+ pnpm test --run 2>&1 | tail -8
433
+ pnpm tsc --noEmit
434
+ ```
435
+
436
+ Expected: all green.
437
+
438
+ - [ ] **Step 5.4:** Commit:
439
+
440
+ ```bash
441
+ git add package.json
442
+ git commit -m "$(cat <<'EOF'
443
+ chore: bump SDK version to 0.2.0
444
+
445
+ Reflects additive sales-workflow polish:
446
+ - typed SalesWorkflowToolName + isSalesWorkflowTool guard
447
+ - onSalesToolCall callback on React + Vue hooks
448
+ - widget status pills for the new tool events
449
+
450
+ Non-breaking; existing consumers continue to work without changes.
451
+
452
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
453
+ EOF
454
+ )"
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Out-of-scope follow-ups
460
+
461
+ - Tool-call-specific entries on the `SalesBotEvent` discriminated union (current solution piggybacks on `tool_call_started`).
462
+ - Operator-customizable pill text (operator sets the "Generating quote…" string per tenant).
463
+ - Localization of pill text.
464
+
465
+ ---
466
+
467
+ ## Self-review notes
468
+
469
+ Spec coverage:
470
+
471
+ - §6.1 typed discriminators — Task 2.
472
+ - §6.2 widget UI hints — Task 4.
473
+ - §6.3 React/Vue hook callbacks — Task 3.
474
+ - §6.5 version bump — Task 5.
475
+
476
+ No placeholders. Each step's intent is spelled out with code blocks; the few places where the implementer must read the existing file before adapting (the widget pill insertion point, the hook subscription pattern) are explicitly flagged.