diff --git a/.changeset/production-hardening.md b/.changeset/production-hardening.md
deleted file mode 100644
index 197fa1f..0000000
--- a/.changeset/production-hardening.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-"@guidekit/core": patch
-"@guidekit/react": patch
-"@guidekit/server": patch
----
-
-Production hardening: stable pipeline telemetry export and DevTools Telemetry tab; LLM/voice proxy permission and origin checks with request validation; example app Redis session-store path and operational docs (rate limits, failure modes, observability).
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 6efc30d..e9309da 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -37,6 +37,7 @@ jobs:
- name: Run live E2E suite
env:
LIVE_LLM: '1'
+ SKIP_LIVE_VOICE: '1'
GUIDEKIT_SECRET: guidekit-example-e2e-secret-32-chars
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
SKIP_NPM_DRY_RUN: '1'
diff --git a/.size-limit.json b/.size-limit.json
index 7f35214..52539ab 100644
--- a/.size-limit.json
+++ b/.size-limit.json
@@ -2,7 +2,7 @@
{
"name": "@guidekit/core (ESM)",
"path": "packages/core/dist/index.js",
- "limit": "85 KB",
+ "limit": "92 KB",
"gzip": true
},
{
diff --git a/AGENTS.md b/AGENTS.md
index 2fce5c9..7de4c3c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -77,27 +77,27 @@ e2e/
└── env.ts # .env.local + LIVE_LLM detection
```
-Voice E2E always mocks the browser Web Speech API — no Deepgram/ElevenLabs in Playwright.
+Voice E2E: contract tier mocks Web Speech in Playwright; live tier uses real Web Speech via Chromium fake-audio-capture (no Deepgram/ElevenLabs).
### E2E coverage matrix (user-facing flows)
| Flow | Contract | Live |
|------|:--------:|:----:|
-| Widget UI / a11y | yes | — |
+| Widget UI / a11y | yes | yes |
| Proxy health / token / LLM | yes | yes |
| Text chat + streaming | mocked | yes |
| Multi-turn memory | — | yes |
| Agent tools (scroll, highlight, navigate, tour, clickElement) | yes | yes |
| Platform Mode (RAG, plugin, cognitive page) | yes | yes |
| Session recovery 401 | yes | yes |
-| Voice (Web Speech mock → LLM) | yes | yes |
-| Custom actions / form / readPage / dismiss | yes | partial |
+| Voice (Web Speech → LLM) | mocked STT | real STT |
+| Custom actions / form / readPage / dismiss | yes | yes |
| STT/TTS proxy key minting | yes | — |
-| Hallucination guard bus event | yes | — |
-| Vanilla IIFE widget | yes | — |
-| Headless custom UI | yes | — |
+| Hallucination guard bus event | yes | yes |
+| Vanilla IIFE widget | yes | yes |
+| Headless custom UI | yes | yes |
-Commands: `pnpm test:e2e:contract` (CI), `pnpm test:e2e:live` (local), `pnpm test:e2e:live:full` (publish gate).
+Commands: `pnpm test:e2e:contract` (CI), `pnpm test:e2e:live` (local), `pnpm test:e2e:live:full` (publish gate). Set `SKIP_LIVE_VOICE=1` to skip the headed real-Web-Speech live voice spec when STT is unavailable.
Before release, run `pnpm check:release` (runs live suite twice for the flake budget). Publish workflow uploads Playwright artifacts on failure.
diff --git a/SECURITY.md b/SECURITY.md
index 644ffcb..4ab13e8 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,7 +4,9 @@
| Version | Supported |
| ------- | --------- |
-| 0.1.x | Yes |
+| 1.x | Yes |
+| 0.3.x | Security fixes only |
+| 0.1.x | No |
## Reporting a Vulnerability
diff --git a/apps/docs/app/docs/getting-started/page.mdx b/apps/docs/app/docs/getting-started/page.mdx
index c3f3bc8..e47ea60 100644
--- a/apps/docs/app/docs/getting-started/page.mdx
+++ b/apps/docs/app/docs/getting-started/page.mdx
@@ -167,6 +167,64 @@ For quick prototyping without server routes, pass API keys directly:
| Create React App | Full |
| Non-React (vanilla JS) | Via `@guidekit/vanilla` |
+## SPA navigation (Next.js App Router)
+
+GuideKit detects URL changes via the Navigation API, `popstate`, and polling. For client-side route changes that bypass full page loads, pass your App Router instance so `navigate()` and post-nav rescans stay reliable:
+
+```tsx
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { GuideKitProvider } from '@guidekit/react';
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+
+ return (
+ router.push(href) } }}
+ >
+ {children}
+
+ );
+}
+```
+
+On each route change, GuideKit clears **PageMemory**, rescans the DOM, and emits `dom:route-change` plus `context:memory-cleared`.
+
+For **in-page DOM swaps** (tabs, modals, virtual lists) without a URL change, call `core.rescanPage()` after updating the UI so the assistant sees fresh sections:
+
+```tsx
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useGuideKitCore } from '@guidekit/react';
+
+export function TabPanel({ activeTab }: { activeTab: string }) {
+ const core = useGuideKitCore();
+
+ useEffect(() => {
+ if (core?.isReady) core.rescanPage();
+ }, [activeTab, core]);
+
+ return
{/* tab content */}
;
+}
+```
+
+## Universal site checklist
+
+1. Proxy mode: `tokenEndpoint` + `/api/guidekit/llm` (never ship LLM keys to the browser)
+2. Optional `contentMap` for product facts the DOM cannot infer
+3. Optional `data-guidekit-target` on critical CTAs (improves accuracy, not required)
+4. `clickableSelectors.allow/deny` for production click boundaries
+5. Redis session store for multi-instance deployments
+6. `hallucinationGuard={true}` + `intelligence={true}` for Platform Mode on public sites
+
+See the example app routes `/plain`, `/spa-rescan`, and `/iframe-test` for unannotated page demos used in contract E2E.
+
## Next Steps
- [Provider Setup](/docs/provider) — Configuration options in depth
diff --git a/apps/docs/app/docs/observability/page.mdx b/apps/docs/app/docs/observability/page.mdx
index ad94c82..571ea20 100644
--- a/apps/docs/app/docs/observability/page.mdx
+++ b/apps/docs/app/docs/observability/page.mdx
@@ -56,6 +56,27 @@ GuideKit guarantees these attribute keys when applicable:
Additional attributes may be added over time, but existing keys are stable.
+## Reliability scorecard (Universal Assistant targets)
+
+Track these signals in contract and live E2E suites when rolling out to production:
+
+| Metric | Contract target | Live target |
+|--------|-----------------|-------------|
+| Claim grounding (hallucination guard clean) | ≥ 90% | ≥ 85% |
+| Highlight accuracy on plain pages | ≥ 85% | ≥ 85% |
+| Tool success (`highlight`, `scroll`, `readPageContent`) | ≥ 95% | ≥ 90% |
+| Prompt tokens per turn (after PageMemory) | −40% vs full prompt | −40% |
+
+**Bus events for context and grounding:**
+
+- `context:memory-rebuild` — PageMemory rebuilt after route or hash change
+- `context:delta` — incremental TurnDelta sent on subsequent turns
+- `element:resolve` — semantic ref resolved to selector (`confidence`, `reason`)
+- `action:confirmation-required` — dangerous click blocked pending user confirmation
+- `validation:corrected` — high-severity hallucination issues appended to response
+
+Use DevTools **Events** or `window.__guidekitTest` in the example app to capture these during E2E debugging.
+
## Debugging workflow
1. Reproduce the slow or failed message in dev with `options={{ debug: true }}`.
diff --git a/apps/example-nextjs/CHANGELOG.md b/apps/example-nextjs/CHANGELOG.md
index d81cc8d..e24d919 100644
--- a/apps/example-nextjs/CHANGELOG.md
+++ b/apps/example-nextjs/CHANGELOG.md
@@ -4,6 +4,18 @@
### Patch Changes
+- Updated dependencies [e504e76]
+ - @guidekit/core@1.1.0
+ - @guidekit/react@1.2.0
+ - @guidekit/server@1.0.2
+ - @guidekit/intelligence@2.0.0
+ - @guidekit/knowledge@2.0.0
+ - @guidekit/plugins@2.0.0
+
+## 0.0.4
+
+### Patch Changes
+
- Updated dependencies [2b44662]
- @guidekit/core@1.0.0
- @guidekit/react@1.0.0
diff --git a/apps/example-nextjs/app/(main)/iframe-test/page.tsx b/apps/example-nextjs/app/(main)/iframe-test/page.tsx
new file mode 100644
index 0000000..edc03cb
--- /dev/null
+++ b/apps/example-nextjs/app/(main)/iframe-test/page.tsx
@@ -0,0 +1,27 @@
+export default function IframeTestPage() {
+ return (
+
+ Iframe Grounding Demo
+ Same-origin iframe content is readable; cross-origin iframes are listed as limitations.
+
+
+
+
+
+ );
+}
diff --git a/apps/example-nextjs/app/(main)/plain/page.tsx b/apps/example-nextjs/app/(main)/plain/page.tsx
new file mode 100644
index 0000000..fc168e5
--- /dev/null
+++ b/apps/example-nextjs/app/(main)/plain/page.tsx
@@ -0,0 +1,34 @@
+export default function PlainPage() {
+ return (
+
+ Plain Website Demo
+ No data-guidekit-target annotations — heuristic DOM scan only.
+
+
+ Introduction
+
+ GuideKit infers sections from headings and landmarks on unannotated pages like this one.
+
+
+
+
+ Product Features
+
+ - Runtime DOM grounding
+ - Incremental page memory
+ - Safe click boundaries
+
+
+
+
+ Account
+
+
+
+
+ );
+}
diff --git a/apps/example-nextjs/app/(main)/spa-rescan/page.tsx b/apps/example-nextjs/app/(main)/spa-rescan/page.tsx
new file mode 100644
index 0000000..22f65d5
--- /dev/null
+++ b/apps/example-nextjs/app/(main)/spa-rescan/page.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useGuideKitCore } from '@guidekit/react';
+
+export default function SpaRescanPage() {
+ const core = useGuideKitCore();
+ const [variant, setVariant] = useState<'a' | 'b'>('a');
+
+ useEffect(() => {
+ if (!core?.isReady || typeof core.rescanPage !== 'function') return;
+ core.rescanPage();
+ }, [variant, core?.isReady, core]);
+
+ return (
+
+ SPA DOM Swap Demo
+ Replace page content without navigation to test rescan + page memory invalidation.
+
+
+
+
+ {variant === 'a' ? (
+
+ Panel Alpha
+ Initial content visible after load.
+
+ ) : (
+
+ Panel Beta
+ Replacement content after DOM swap — hash should change on rescan.
+
+ )}
+
+
+ );
+}
diff --git a/apps/example-nextjs/app/api/guidekit/test/invalidate-session/route.ts b/apps/example-nextjs/app/api/guidekit/test/invalidate-session/route.ts
new file mode 100644
index 0000000..84e4201
--- /dev/null
+++ b/apps/example-nextjs/app/api/guidekit/test/invalidate-session/route.ts
@@ -0,0 +1,43 @@
+import { clearSessionKeys, validateSessionToken } from '@guidekit/server';
+import { NextResponse } from 'next/server';
+import { guidekitSessionStore } from '../../../../../lib/guidekit-session-store';
+
+/**
+ * Dev/E2E only — clears server-side session keys so the next LLM proxy call
+ * returns 401 and the client can exercise real session recovery.
+ */
+export async function POST(request: Request): Promise {
+ if (process.env.NODE_ENV !== 'development') {
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
+ }
+
+ const signingSecret = process.env.GUIDEKIT_SECRET;
+ if (!signingSecret) {
+ return NextResponse.json({ error: 'GUIDEKIT_SECRET not configured' }, { status: 500 });
+ }
+
+ let token: string | undefined;
+ const auth = request.headers.get('Authorization');
+ if (auth?.startsWith('Bearer ')) {
+ token = auth.slice(7).trim();
+ } else {
+ try {
+ const body = (await request.json()) as { token?: string };
+ token = typeof body.token === 'string' ? body.token.trim() : undefined;
+ } catch {
+ token = undefined;
+ }
+ }
+
+ if (!token) {
+ return NextResponse.json({ error: 'Missing session token' }, { status: 400 });
+ }
+
+ const validation = await validateSessionToken(token, signingSecret);
+ if (!validation.valid || !validation.payload) {
+ return NextResponse.json({ error: validation.error ?? 'Invalid token' }, { status: 401 });
+ }
+
+ const deleted = await clearSessionKeys(validation.payload.sessionId, guidekitSessionStore);
+ return NextResponse.json({ ok: true, deleted });
+}
diff --git a/apps/example-nextjs/app/guidekit-test-bridge.tsx b/apps/example-nextjs/app/guidekit-test-bridge.tsx
index 42fab2f..f80a8e2 100644
--- a/apps/example-nextjs/app/guidekit-test-bridge.tsx
+++ b/apps/example-nextjs/app/guidekit-test-bridge.tsx
@@ -12,6 +12,7 @@ declare global {
events: BusEvent[];
waitForEvent: (name: string, timeoutMs?: number) => Promise;
waitForReady: (timeoutMs?: number) => Promise;
+ getPageModel: () => unknown;
addKnowledgeDocument: (doc: KnowledgeDocument) => void;
removeKnowledgeDocument: (documentId: string) => void;
clear: () => void;
@@ -53,6 +54,10 @@ export function GuideKitTestBridge() {
const unsubLlmEnd = core.bus.on('llm:response-end', (data) => {
push('llm:response-end', data);
});
+ const unsubAny = core.bus.onAny((data, name) => {
+ if (name === 'validation:complete' || name === 'llm:response-end') return;
+ push(name, data);
+ });
window.__guidekitTest = {
events,
@@ -90,6 +95,7 @@ export function GuideKitTestBridge() {
}
}, 50);
}),
+ getPageModel: () => core.pageModel,
addKnowledgeDocument: (doc) => {
core.addKnowledgeDocument(doc);
},
@@ -104,6 +110,7 @@ export function GuideKitTestBridge() {
return () => {
unsubValidation();
unsubLlmEnd();
+ unsubAny();
delete window.__guidekitTest;
};
}, [core]);
diff --git a/apps/example-nextjs/app/providers.tsx b/apps/example-nextjs/app/providers.tsx
index a61c538..da65eca 100644
--- a/apps/example-nextjs/app/providers.tsx
+++ b/apps/example-nextjs/app/providers.tsx
@@ -37,7 +37,7 @@ export function Providers({ children }: { children: ReactNode }) {
debug: process.env.NODE_ENV === 'development',
mode: voiceEnabled ? 'voice' : 'text',
clickableSelectors: {
- allow: ['#name', '#email', '#message', 'input', 'textarea'],
+ deny: ['[type="submit"]', '[data-guidekit-no-click]'],
},
}}
>
diff --git a/apps/example-nextjs/package.json b/apps/example-nextjs/package.json
index 4de6d92..9c7fb4b 100644
--- a/apps/example-nextjs/package.json
+++ b/apps/example-nextjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@guidekit/example-nextjs",
- "version": "0.0.3",
+ "version": "0.0.4",
"private": true,
"scripts": {
"dev": "next dev -p 3099",
diff --git a/apps/example-nextjs/public/vanilla-csp-demo.html b/apps/example-nextjs/public/vanilla-csp-demo.html
new file mode 100644
index 0000000..a8fe7c6
--- /dev/null
+++ b/apps/example-nextjs/public/vanilla-csp-demo.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ GuideKit Vanilla CSP Demo
+
+
+
+ Vanilla CSP Demo
+ Widget loaded under a strict Content-Security-Policy (self-hosted scripts only).
+
+
+
+
+
+
diff --git a/docs/ARCHITECTURE_VISION.md b/docs/ARCHITECTURE_VISION.md
new file mode 100644
index 0000000..378b60a
--- /dev/null
+++ b/docs/ARCHITECTURE_VISION.md
@@ -0,0 +1,700 @@
+# GuideKit Architecture Vision
+
+> **Ultimate goal:** Once integrated, GuideKit becomes a reliable AI assistant embedded in any website — explaining what the user sees, highlighting the right UI, guiding them through flows, and answering questions grounded in the live page, not guesses.
+
+This document is the end-to-end architecture reference for building toward that goal. It synthesizes the current SDK state, target design, reliability model, and phased roadmap.
+
+---
+
+## Table of Contents
+
+1. [The Ultimate Goal](#1-the-ultimate-goal)
+2. [Product Promise](#2-product-promise)
+3. [What Makes GuideKit Different](#3-what-makes-guidekit-different)
+4. [System Architecture](#4-system-architecture)
+5. [End-to-End Data Flow](#5-end-to-end-data-flow)
+6. [Context Capture Strategy](#6-context-capture-strategy)
+7. [LLM Orchestration and Agent Tools](#7-llm-orchestration-and-agent-tools)
+8. [Reliability and Accuracy Model](#8-reliability-and-accuracy-model)
+9. [Token Cost and Performance](#9-token-cost-and-performance)
+10. [Security Boundary](#10-security-boundary)
+11. [Extension Architecture](#11-extension-architecture)
+12. [Agent Team and Ownership](#12-agent-team-and-ownership)
+13. [Phased Roadmap](#13-phased-roadmap)
+14. [Success Metrics](#14-success-metrics)
+15. [Testing Strategy](#15-testing-strategy)
+16. [Key Files Reference](#16-key-files-reference)
+
+---
+
+## 1. The Ultimate Goal
+
+GuideKit is a **multi-package AI guidance SDK** for web applications. The north star is simple:
+
+> **Drop GuideKit into any website. The agent understands what is on screen, helps the user navigate and complete tasks, and never invents UI that does not exist.**
+
+### What the agent must do
+
+| Capability | Description |
+|------------|-------------|
+| **Explain** | Describe page sections, features, and content in plain language |
+| **Show** | Scroll to relevant areas, start guided tours, surface visible context |
+| **Highlight** | Spotlight specific elements with tooltips so users know exactly where to look |
+| **Answer** | Respond to free-form questions grounded in the current page state |
+| **Act (safely)** | Click, navigate, and execute developer-registered actions within guardrails |
+| **Persist** | Maintain conversation memory across turns and page navigations within a session |
+
+### What the agent is not
+
+- Not a generic chat widget that guesses from a URL
+- Not a crawler that ingests an entire multi-page site at once
+- Not a replacement for product documentation (though it can augment it via RAG)
+- Not omniscient inside cross-origin iframes, canvas apps, or inaccessible DOM
+
+The agent's knowledge boundary is **what the browser can see and what the developer exposes** — that is the correct and reliable model.
+
+---
+
+## 2. Product Promise
+
+When a developer integrates GuideKit (React/Next.js primary; vanilla IIFE secondary), their users get:
+
+1. **Instant page awareness** — the SDK scans the rendered DOM and builds a structured `PageModel` within budget (~5KB compact representation).
+2. **Visual guidance** — spotlight overlays, tooltips, scroll, and tours that point at real elements.
+3. **Grounded answers** — every LLM turn includes page context; tools verify and expand context on demand.
+4. **Secure by default** — API keys stay on the server; the browser holds only session tokens.
+5. **Observable behavior** — pipeline telemetry, validation events, and E2E coverage make reliability measurable.
+
+### Primary integration surface (v1)
+
+- **React/Next.js** — `` + `/api/guidekit/*` proxy routes
+- **Vanilla embed** — IIFE bundle for script-tag integration on any site
+
+---
+
+## 3. What Makes GuideKit Different
+
+| Generic chat widget | GuideKit |
+|---------------------|----------|
+| Text-only responses | Visual guidance (highlight, tour, scroll) |
+| Static or manual context | Live DOM intelligence with mutation-aware rescans |
+| API keys in browser (risky) | Proxy mode: JWT session tokens, keys on server |
+| One-shot prompts | Multi-round tool loop (read → highlight → navigate) |
+| No grounding validation | Hallucination guard validates claims against `PageModel` |
+| Monolithic bundle | Composable packages: core, react, server, optional extensions |
+
+GuideKit is an **SDK-first guidance engine**, not a copy-paste chat component. Extensions (`intelligence`, `knowledge`, `plugins`) plug in via dynamic imports without bloating the core facade.
+
+---
+
+## 4. System Architecture
+
+```
+guidekit/
+├── packages/
+│ ├── core/ # Engine: DOM, context, pipeline, LLM, tools, voice
+│ ├── react/ # Provider, hooks, Shadow DOM widget
+│ ├── server/ # Token auth, session store, LLM/voice proxy
+│ ├── intelligence/ # Semantic page analysis, hallucination guard
+│ ├── knowledge/ # BM25/TF-IDF client-side RAG
+│ ├── plugins/ # Plugin registry and pipeline hooks
+│ ├── vanilla/ # IIFE script-tag bundle
+│ └── cli/ # init, doctor, generate-secret
+├── apps/
+│ ├── example-nextjs/ # Reference Next.js integration (proxy mode)
+│ └── docs/ # Public documentation (Nextra)
+└── e2e/ # Contract (CI) + Live (publish gate) Playwright tests
+```
+
+### Layer responsibilities
+
+```mermaid
+flowchart TB
+ subgraph browser [Browser Client]
+ widget[GuideKitWidget]
+ core[GuideKitCore]
+ scanner[DOMScanner]
+ context[ContextManager]
+ pipeline[PipelineOrchestrator]
+ tools[ToolExecutor]
+ visual[VisualGuidance]
+ widget --> core
+ core --> scanner
+ core --> context
+ core --> pipeline
+ pipeline --> tools
+ tools --> visual
+ end
+
+ subgraph server [Server Proxy]
+ tokenRoute["/api/guidekit/token"]
+ llmRoute["/api/guidekit/llm"]
+ sessionStore[SessionStore]
+ tokenRoute --> sessionStore
+ llmRoute --> sessionStore
+ end
+
+ subgraph providers [Upstream Providers]
+ llm[LLM Provider]
+ stt[STT Provider]
+ tts[TTS Provider]
+ end
+
+ pipeline -->|"JWT Bearer"| llmRoute
+ llmRoute --> llm
+ core -->|"mint session"| tokenRoute
+```
+
+| Package | Owns |
+|---------|------|
+| `@guidekit/core` | DOM scan, `PageModel`, context assembly, LLM loop, built-in tools, voice primitives, pipeline |
+| `@guidekit/react` | Provider, hooks, Shadow DOM widget — no duplicated business logic |
+| `@guidekit/server` | Session store, JWT auth, rate limit, Next.js adapter, LLM/STT/TTS proxy |
+| `@guidekit/intelligence` | Semantic enrichment, hallucination guard |
+| `@guidekit/knowledge` | Document retrieval (BM25/TF-IDF) |
+| `@guidekit/plugins` | Custom tools, context providers, pipeline hooks |
+
+### Design rules (non-negotiable)
+
+1. **Core facade stays thin** — `packages/core/src/core.ts` is a facade (~400 LOC target); subsystems live in `packages/core/src/core/`.
+2. **No hard deps on Tier B packages** — `intelligence`, `knowledge`, `plugins` load via dynamic import in `pipeline/extensions.ts`.
+3. **Proxy by default** — never expose LLM API keys in the browser.
+4. **Measure everything** — telemetry spans and token budgets are first-class.
+
+---
+
+## 5. End-to-End Data Flow
+
+Every user message traverses the v2 pipeline:
+
+```
+scan → enrich → retrieve → context → cognize → llm → validate → render
+```
+
+### Stage-by-stage
+
+| Stage | What happens | Owner |
+|-------|--------------|-------|
+| **scan** | Read cached `PageModel` from `DOMScanner` (continuously updated via MutationObserver) | core/dom |
+| **enrich** | Optional semantic scan → `SemanticPageModel` (components, heading outline, errors, flow state) | intelligence |
+| **retrieve** | Optional RAG: append knowledge section from indexed documents | knowledge |
+| **context** | `ContextManager.buildSystemPrompt()` assembles role + page + sections + tools; enforce token budget | core/context |
+| **cognize** | Optional cognitive planning (tool round limits, prompt additions) | core/cognitive |
+| **llm** | `ToolExecutor.executeWithToolsStream()` — multi-round streaming with tool calls | core/llm |
+| **validate** | Hallucination guard checks response claims against `PageModel` | intelligence |
+| **render** | Widget updates via agent state; spotlight/tour side effects from tool execution | react/widget |
+
+### Continuous vs per-turn work
+
+| Always on (background) | Per user message (foreground) |
+|------------------------|-------------------------------|
+| DOM scan + MutationObserver | Pipeline stages |
+| PageModel cache + hash | System prompt assembly |
+| IntersectionObserver visibility | LLM call via proxy |
+| Session memory (sessionStorage) | Tool execution + validation |
+
+**Key insight:** The DOM is scanned continuously; the LLM receives a **bounded snapshot** of what matters for the current turn. This is the foundation for incremental context (see Section 6).
+
+---
+
+## 6. Context Capture Strategy
+
+### Current state (implemented)
+
+`DOMScanner` (`packages/core/src/dom/index.ts`) builds a `PageModel`:
+
+- **Sections** — semantic tags, landmarks, scored by visibility/interactivity/depth (top 20)
+- **Navigation** — links inside `