diff --git a/docs/superpowers/plans/2026-08-31-threadplane-canonical-privacy-policy.md b/docs/superpowers/plans/2026-08-31-threadplane-canonical-privacy-policy.md new file mode 100644 index 000000000..a0c6e08e9 --- /dev/null +++ b/docs/superpowers/plans/2026-08-31-threadplane-canonical-privacy-policy.md @@ -0,0 +1,338 @@ +# Threadplane Canonical Privacy Policy Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace Threadplane's public analytics promises and dedicated documentation with one canonical `/privacy` policy, while ensuring no rendered website surface or public response contains the case-insensitive word `telemetry`. + +**Architecture:** Keep internal package names, source identifiers, environment variables, logs, and engineering tests intact. Remove the dedicated public docs library, project a sanitized subset of TypeDoc data into the public website, redirect retired routes, and enforce the public-copy boundary in both unit tests and a production crawl. + +**Tech Stack:** Next.js 16, React 19, Nx 22, Vitest 4, Playwright, TypeDoc, MDX. + +**Spec:** `docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md` sections K, M.59–M.65, and N/PR 4. + +**Dependency:** Land this plan before enabling default-on runtime analytics. It has no Neon or provider dependency. + +**Merge order and dirty-worktree rule:** Land this plan first, then the lifecycle/control-plane plan, then the runtime plan. Before the PR, record `git status --short`; review `git diff -- ` and stage only explicit owned paths or hunks. Website, lockfile, and CI changes already present in the worktree are not implicitly part of this plan. + +--- + +## Verified current state + +- There is no `/privacy` route, footer privacy link, or legacy redirect. +- `docsConfig` exposes six HTML routes and six `/api/markdown/telemetry/...` routes from `apps/website/content/docs/telemetry/`. +- That config also feeds navigation, docs search, static params, sitemap entries, and markdown lookup; there is no independent search-index file. +- Public occurrences span the dedicated library, three blogs, fourteen narrative docs, three generated API files, the home/pilot-to-prod promise surfaces, `llms.txt`, `llms-full.txt`, and `/api/ingest` error bodies. +- `OpenTelemetry` on Pilot-to-Prod matches the required case-insensitive output scan. +- `YesWall` publishes an install/no-phone-home guarantee without the target word, so it is also removed under the approved no-install-guarantees decision. +- Internal identifiers such as `@threadplane/telemetry`, `AgentRuntimeTelemetry*`, `TelemetryIngestPayload`, and `TPLANE_TELEMETRY_*` are outside this public-copy migration. + +## Public copy contract + +The canonical policy covers submitted information and communications; website and product analytics; operational/research/support/approved-outreach purposes; Vercel, Neon, PostHog, Resend, Google Workspace, and Anthropic; email opt-out and reply handling; indefinite default retention; deletion requests; security; international processing; changes; and `brian@threadplane.ai` contact information. + +It must not contain an event/property catalog, installation behavior claim, exclusion/"never collected" list, "what we won't do" positioning, or an absolute guarantee. The absence of the word from rendered public output is a product-copy decision, not a request to rename internal APIs. + +--- + +## Task 1: Add the canonical policy, sitemap entry, and footer link + +**Files:** + +- Create: `apps/website/src/app/privacy/page.tsx` +- Create: `apps/website/src/app/privacy/page.spec.tsx` +- Modify: `apps/website/src/lib/site-metadata.ts` +- Modify: `apps/website/src/lib/sitemap-dates.spec.ts` +- Modify: `apps/website/src/components/shared/Footer.tsx` +- Modify: `apps/website/e2e/website.spec.ts` + +- [ ] **Step 1: Write failing policy tests.** Render the route and assert the policy has canonical metadata, names the approved data categories/processors, says default retention is indefinite, explains deletion/opt-out/contact, and omits the prohibited promises/catalogs. + +- [ ] **Step 2: Write failing discovery tests.** Assert `/privacy` is in `getSitemapRoutes()`/the sitemap and that the global footer links to it. + +- [ ] **Step 3: Run the focused red test.** + +```bash +npx nx test website -- --run apps/website/src/app/privacy/page.spec.tsx apps/website/src/lib/sitemap-dates.spec.ts +``` + +Expected: FAIL because the route and sitemap entry do not exist. + +- [ ] **Step 4: Implement the policy route.** Use the site's existing metadata/layout primitives. Keep the copy factual and general; do not add a separate analytics disclosure page. + +- [ ] **Step 5: Add discovery surfaces.** Add `/privacy` to the static sitemap inventory and a bottom-bar footer link with the existing footer CTA tracking pattern. + +- [ ] **Step 6: Re-run the focused test.** + +```bash +npx nx test website -- --run apps/website/src/app/privacy/page.spec.tsx apps/website/src/lib/sitemap-dates.spec.ts +``` + +Expected: PASS. + +--- + +## Task 2: Remove promise-oriented marketing surfaces + +**Files:** + +- Delete: `apps/website/src/components/landing/Promises.tsx` +- Modify: `apps/website/src/app/page.tsx` +- Modify: `apps/website/src/app/pilot-to-prod/page.tsx` +- Modify: `apps/website/src/components/landing/FinalCTA.tsx` +- Modify: `apps/website/src/components/landing/FinalCTA.spec.tsx` +- Modify: `apps/website/src/components/landing/YesWall.tsx` +- Modify: `apps/website/src/components/landing/YesWall.spec.tsx` +- Modify: `apps/website/src/app/solutions/[slug]/page.tsx` +- Modify: `apps/website/src/app/render/page.tsx` +- Modify: `apps/website/src/styles/landing.css` +- Modify: `apps/website/e2e/website.spec.ts` + +- [ ] **Step 1: Make the regression tests express the new surface.** Assert `FinalCTA` has no caption, `YesWall` has fifteen current questions and no install/no-phone-home claim, and the home and Pilot-to-Prod routes do not render a Promises section. + +- [ ] **Step 2: Run the focused red test.** + +```bash +npx nx test website -- --run apps/website/src/components/landing/FinalCTA.spec.tsx apps/website/src/components/landing/YesWall.spec.tsx +``` + +Expected: FAIL on the default caption, old question count, and install claim. + +- [ ] **Step 3: Delete the whole Promises component and both call sites.** Do not preserve a reduced "what we won't do" section. + +- [ ] **Step 4: Remove the `FinalCTA.caption` prop, default, markup, call-site override, and orphaned `.final-cta-caption` rules.** + +- [ ] **Step 5: Remove the `YesWall` install/no-phone-home item.** Derive its count from the remaining data or update it to fifteen so the label cannot drift. + +- [ ] **Step 6: Reword remaining rendered references.** Change Pilot-to-Prod's `OpenTelemetry hooks` to `Distributed tracing hooks`; use observability/render-event language on the Render page. + +- [ ] **Step 7: Remove the orphaned Promises CSS block from `apps/website/src/styles/landing.css`.** + +- [ ] **Step 8: Re-run the focused tests.** + +```bash +npx nx test website -- --run apps/website/src/components/landing/FinalCTA.spec.tsx apps/website/src/components/landing/YesWall.spec.tsx +``` + +Expected: PASS. + +--- + +## Task 3: Retire the dedicated docs library and redirect every old route + +**Files:** + +- Delete: `apps/website/content/docs/telemetry/api/api-docs.json` +- Delete: `apps/website/content/docs/telemetry/getting-started/installation.mdx` +- Delete: `apps/website/content/docs/telemetry/getting-started/introduction.mdx` +- Delete: `apps/website/content/docs/telemetry/guides/browser.mdx` +- Delete: `apps/website/content/docs/telemetry/guides/node.mdx` +- Delete: `apps/website/content/docs/telemetry/guides/privacy-and-opt-out.mdx` +- Delete: `apps/website/content/docs/telemetry/reference/events.mdx` +- Modify: `apps/website/next.config.ts` +- Modify: `apps/website/next.config.spec.ts` +- Modify: `apps/website/src/lib/docs-config.ts` +- Modify: `apps/website/src/lib/docs.spec.ts` +- Modify: `apps/website/src/app/docs/page.tsx` +- Modify: `apps/website/src/components/docs/LibraryMark.tsx` +- Modify: `apps/website/src/components/docs/LibraryMark.spec.tsx` +- Modify: `apps/website/e2e/docs.spec.ts` +- Modify: `apps/website/e2e/website.spec.ts` + +- [ ] **Step 1: Add failing redirect and inventory tests.** Cover both exact roots and wildcard descendants for `/docs/telemetry` and `/api/markdown/telemetry`; assert the retired library is absent from slugs, docs cards, search, and sitemap inventory. + +- [ ] **Step 1a: Preserve existing configuration coverage.** Extend `next.config.spec.ts`; keep its PostHog rewrite assertions intact while adding redirect assertions. + +- [ ] **Step 2: Run the focused red test.** + +```bash +npx nx test website -- --run apps/website/src/lib/docs.spec.ts apps/website/next.config.spec.ts apps/website/src/components/docs/LibraryMark.spec.tsx +``` + +Expected: FAIL because the library remains and redirects are absent. + +- [ ] **Step 3: Add permanent redirects.** Add exact-root and `:path*` rules in `next.config.ts`, all targeting `/privacy`, for both public docs and markdown API routes. + +- [ ] **Step 4: Delete the seven dedicated public files.** Remove the `LibraryId` member/config block, docs home card, and `LibraryMark` mapping. Search/static params/sitemap/markdown lookup then lose the library through the existing shared configuration. + +- [ ] **Step 5: Update existing tests and overflow/crawl matrices.** Replace old route expectations with redirect and absence assertions. + +- [ ] **Step 6: Re-run the focused tests.** + +```bash +npx nx test website -- --run apps/website/src/lib/docs.spec.ts apps/website/next.config.spec.ts apps/website/src/components/docs/LibraryMark.spec.tsx +``` + +Expected: PASS. + +--- + +## Task 4: Clean public narrative copy and response bodies + +**Files:** + +- Modify: `apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx` +- Modify: `apps/website/content/blog/2026-08-09-agentic-ui-in-angular-production-patterns.mdx` +- Modify: `apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx` +- Modify: `apps/website/content/docs/ag-ui/api/inject-agent.mdx` +- Modify: `apps/website/content/docs/ag-ui/api/provide-agent.mdx` +- Modify: `apps/website/content/docs/ag-ui/api/to-agent.mdx` +- Modify: `apps/website/content/docs/ag-ui/concepts/architecture.mdx` +- Modify: `apps/website/content/docs/ag-ui/getting-started/installation.mdx` +- Modify: `apps/website/content/docs/ag-ui/reference/event-mapping.mdx` +- Modify: `apps/website/content/docs/chat/guides/error-handling.mdx` +- Modify: `apps/website/content/docs/chat/guides/lifecycle.mdx` +- Modify: `apps/website/content/docs/chat/guides/thread-routing.mdx` +- Modify: `apps/website/content/docs/langgraph/api/provide-agent.mdx` +- Modify: `apps/website/content/docs/langgraph/concepts/agent-contract.mdx` +- Modify: `apps/website/content/docs/langgraph/getting-started/introduction.mdx` +- Modify: `apps/website/content/docs/langgraph/guides/lifecycle.mdx` +- Modify: `apps/website/content/docs/render/guides/lifecycle.mdx` +- Modify: `apps/website/src/app/llms.txt/route.ts` +- Modify: `apps/website/src/app/llms-full.txt/route.ts` +- Modify: `apps/website/src/app/api/ingest/route.ts` +- Modify: `apps/website/src/app/api/ingest/route.spec.ts` +- Create: `apps/website/src/lib/public-copy.spec.ts` + +- [ ] **Step 1: Add a failing content scan.** Scan public blog/docs MDX and generated API JSON case-insensitively. Also reject the approved banned claims: `phone home`, `installation is inert`, `off by default`, and `what we won't do`. + +- [ ] **Step 2: Add API response assertions.** Invalid, unconfigured, and failed `/api/ingest` responses must say `event payload`/`event ingest`, while internal type names and log prefixes remain untouched. + +- [ ] **Step 3: Run the focused red test.** + +```bash +npx nx test website -- --run apps/website/src/lib/public-copy.spec.ts apps/website/src/app/api/ingest/route.spec.ts +``` + +Expected: FAIL and identify the remaining public content/response strings. + +- [ ] **Step 4: Rewrite the three blogs and fourteen narrative docs.** Use observability, instrumentation, diagnostics, logging, or neutral lifecycle language. Delete the three lifecycle guides' absolute privacy sections instead of relabeling them. Remove obsolete links and config rows. + +- [ ] **Step 5: Remove the package from both LLM routes.** Delete the JSON import and `API_DOCS` entry from `llms-full.txt`; do not emit an alternative dedicated policy catalog. + +- [ ] **Step 6: Reword only public ingest response strings.** Use `Invalid event payload`, `Event ingest is not configured`, and `Event ingest failed`; keep internal source identifiers intact. + +- [ ] **Step 7: Re-run the focused tests.** + +```bash +npx nx test website -- --run apps/website/src/lib/public-copy.spec.ts apps/website/src/app/api/ingest/route.spec.ts +``` + +Expected: generated API JSON may still fail; all hand-authored content and public response assertions pass. + +--- + +## Task 5: Project internal TypeDoc APIs safely into public docs + +**Files:** + +- Create: `apps/website/scripts/public-doc-projection.ts` +- Create: `apps/website/scripts/public-doc-projection.spec.ts` +- Modify: `apps/website/scripts/generate-api-docs.ts` +- Modify: `apps/website/scripts/generate-narrative-docs.ts` +- Verify/regenerate: `apps/website/public/AGENTS.md` +- Verify/regenerate: `apps/website/public/CLAUDE.md` +- Modify generated: `apps/website/content/docs/ag-ui/api/api-docs.json` +- Modify generated: `apps/website/content/docs/chat/api/api-docs.json` +- Modify generated: `apps/website/content/docs/langgraph/api/api-docs.json` + +- [ ] **Step 1: Write projection tests before the implementation.** Feed representative TypeDoc objects containing affected entry names, property names, types, signatures, and description sentences. Assert the public projection omits the affected structure, retains unrelated data, does not mutate its input, and throws if serialized output still contains the blocked term. + +- [ ] **Step 2: Run the red projection test.** + +```bash +npx nx test website -- --run apps/website/scripts/public-doc-projection.spec.ts +``` + +Expected: FAIL because the projection module does not exist. + +- [ ] **Step 3: Implement the pure public projection.** Filter public TypeDoc output only; do not rename `AgentRuntimeTelemetry*`, config fields, or package exports in source. + +- [ ] **Step 4: Wire the API generator.** Remove the retired library configuration, apply the projection before writing every remaining public JSON file, and validate the serialized output. + +- [ ] **Step 5: Guard narrative generation.** Limit reads to the six remaining public API directories and reject generated MDX before writing if it crosses the boundary. + +- [ ] **Step 6: Run the projection test green.** + +```bash +npx nx test website -- --run apps/website/scripts/public-doc-projection.spec.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Regenerate deterministic API docs only.** Do not run the Anthropic-backed narrative generator for this PR. + +```bash +npm run generate-api-docs +``` + +Expected: six public API-doc outputs remain, no retired output is recreated, and no generated JSON contains the blocked term. + +- [ ] **Step 8: Run the deterministic agent-context generator and scan its public files.** The current output has no occurrence, but the release contract explicitly covers generated public context. + +```bash +npm run generate-agent-context +rg -n -i 'telemetry' apps/website/public/AGENTS.md apps/website/public/CLAUDE.md +``` + +Expected: the generator succeeds, committed outputs are current, and `rg` returns no matches. + +--- + +## Task 6: Add a production-output crawl and finish the rollout gate + +**Files:** + +- Create: `apps/website/e2e/public-copy.spec.ts` +- Modify: `apps/website/playwright.config.ts` +- Modify: `.github/workflows/ci.yml` only if the existing website E2E job does not already execute all specs + +- [ ] **Step 1: Add the E2E boundary test.** Crawl every sitemap URL plus an explicit registry of non-indexed public routes, `/AGENTS.md`, `/CLAUDE.md`, `/llms.txt`, `/llms-full.txt`, representative `/api/markdown` output, and public `/api/ingest` errors. Assert no case-insensitive occurrence; assert the footer link, docs-search absence, canonical policy, and all legacy redirects. Future public routes such as `/connect` must be added to the non-indexed registry in the same PR that creates them. + +- [ ] **Step 1a: Add a mandatory production-server mode.** The current Playwright config starts `next dev`. Add a `WEBSITE_E2E_MODE=production` branch whose web server serves the already-completed Nx production build (for example `npx next start . --hostname ... --port ...` from the correct built app directory). Keep the existing dev mode for ordinary tests, but the public-output gate must use production mode. + +- [ ] **Step 2: Run the targeted E2E red test against a production build.** + +```bash +npx nx build website --configuration=production --skip-nx-cache +WEBSITE_E2E_MODE=production npx nx e2e website -- --grep "public copy boundary" +``` + +Expected initially: FAIL with any remaining rendered or response occurrence. + +- [ ] **Step 3: Fix only the reported public boundary leaks.** Do not expand this into an internal API rename. + +- [ ] **Step 4: Run the complete website verification.** + +```bash +npx nx test website +npx nx lint website +npx nx build website --configuration=production --skip-nx-cache +WEBSITE_E2E_MODE=production npx nx e2e website +``` + +Expected: all pass. + +- [ ] **Step 5: Run final source and diff checks.** + +```bash +rg -n -i 'telemetry' apps/website/content/blog apps/website/content/docs apps/website/public/AGENTS.md apps/website/public/CLAUDE.md +git diff --check +git status --short +``` + +Expected: `rg` returns no matches; `git diff --check` passes; status contains only intended website changes plus the pre-existing unrelated dirty files. + +- [ ] **Step 6: Preview rollout.** Crawl the preview, verify all six docs redirects plus markdown wildcard handling, inspect `/privacy` metadata, footer/search, LLM endpoints, and public API responses. + +- [ ] **Step 7: Create one logical commit after all verification.** Repository guidance forbids mid-task commits. Re-record `git status --short`, inspect `git diff -- `, stage only those paths/hunks, verify `git diff --cached`, and then commit. Never use `git add apps/website`, `git add .`, or include unrelated pre-existing cockpit/lockfile/plan changes. + +--- + +## Acceptance checklist + +- `/privacy` is the sole public policy surface for analytics/data handling. +- Every former docs and markdown route redirects permanently to `/privacy`. +- Home and Pilot-to-Prod do not render Promises; `FinalCTA` has no caption; `YesWall` has no install guarantee. +- Public HTML, generated JSON, sitemap/LLM output, markdown responses, and public API response bodies contain no case-insensitive occurrence. +- Internal engineering identifiers and package APIs remain source-compatible. +- The public policy states indefinite default retention and deletion handling without catalogs, exclusions, or absolute promises. diff --git a/docs/superpowers/plans/2026-08-31-threadplane-runtime-analytics-v1.md b/docs/superpowers/plans/2026-08-31-threadplane-runtime-analytics-v1.md new file mode 100644 index 000000000..6ba56de6c --- /dev/null +++ b/docs/superpowers/plans/2026-08-31-threadplane-runtime-analytics-v1.md @@ -0,0 +1,568 @@ +# Threadplane Runtime Analytics V1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make runtime analytics default-on only after real product operation, emit five strict value milestones through a hardened first-party gateway, preserve inert package installation/import, and link a project to a person only through an explicit one-time claim. + +**Architecture:** `libs/telemetry` owns the closed wire contract, lazy browser/Node project identity, session identity, claim secret, controls, debug mode, and nonblocking first-party transport. Chat supplies the default sink; LangGraph, AG-UI, and Render report semantic success facts through existing lifecycle seams. The website gateway rejects open payloads, registers claim hashes first-write-wins, writes only set-based business projections to Neon, and forwards pseudonymous events to PostHog. Public browser keys and project IDs are attribution, never authentication. + +**Tech Stack:** Nx 22, npm workspaces, Angular 21, TypeScript 5.9, Vitest 4, Next.js 16, Neon Postgres, PostHog JS/Node, Linux `strace`, npm/pnpm/Yarn/Bun install fixtures. + +**Spec:** `docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md` sections E/P0.6–P0.8, P1.3–P1.4, F, H, K–M, and N/PR 5. + +**Dependencies:** Requires the lifecycle plan's `libs/growth`, `growth_projects`, `growth_activity`, contact approval transaction, and migrations 0002/0003. The canonical privacy-policy plan must be deployed before default-on capture is enabled. This plan owns migration 0004 and the `/connect` claim flow. + +**Merge order and dirty-worktree rule:** Apply the privacy plan first, the lifecycle/control-plane plan second, and this runtime plan third. Before each PR, record `git status --short`; review `git diff -- ` and stage only explicit owned paths or hunks. `package-lock.json`, CI files, website files, and PostHog assets may already contain earlier-plan changes and must be extended rather than replaced. + +--- + +## Verified current state and exact seams + +- The six release packages (`chat`, `langgraph`, `ag-ui`, `render`, `a2ui`, `telemetry`) currently have no lifecycle hooks, `bin`, Scarf dependency, or install-time analytics. Existing tests inspect manifests, but there is no offline four-manager/network-syscall proof. +- `libs/telemetry` currently exposes open `tplane:*` mechanics, process-memory Node identity, direct public-key PostHog transport, opt-in browser service, and construction/request/start/end/error events. +- `apps/website/src/app/api/ingest/route.ts` accepts any `tplane:` event, arbitrary properties, and caller-controlled `distinctId`; it has no byte limit, claim registration, rate budget, or durable projection. +- `scripts/rate-limit.ts` persists raw IP rows and fails open. It must be migrated for both existing demo and analytics callers. +- Website PostHog currently allows automatic/default capture and `person_profiles: 'always'`; server conversions derive identity from unsalted email hashes/domain/company. +- LangGraph success facts belong in `stream-manager.bridge.ts` around the first decoded event and `finalizeClosedAttempt(...) === 'success'`. Restore-only persistence is the initial `refreshHistory(false)`, not post-run history. Interrupt handling is success after resume, not submission. +- AG-UI connection is first `RUN_STARTED`; completion is successful `RUN_FINISHED`; interrupted/paused/transport-close-only outcomes do not count. +- Render's trustworthy seam is `render-element.component.ts` after `mountedReal` observes a ready registered component. `render-spec.component.ts` `ngOnInit` only proves a wrapper/spec exists. +- `libs/chat/src/lib/agent/runtime-telemetry.ts` is the adapter boundary to retain and narrow; `AgentOptions.telemetry?: sink | false` already exists. + +## Locked v1 contract + +Every public request has only `event_id`, `event`, `schema_version`, `occurred_at`, `sdk`, `project_id`, optional `project_claim_hash`, `session_id`, and `properties`. Limits: 8 KiB raw body, object depth three, no arrays, UUID IDs, schema version 1, timestamp within ±24 hours, strict SemVer up to 64 characters, SDK name from the six published packages, and a 43-character unpadded base64url SHA-256 claim hash. + +Every properties object contains: + +```ts +type CommonProperties = { + transport: 'langgraph' | 'ag-ui' | 'custom'; + surface: 'agent' | 'chat' | 'render'; +}; +``` + +The exact discriminated additions are: + +```ts +type ActivationPayload = + | { event: 'transport.connected'; properties: CommonProperties } + | { event: 'runtime.first_stream_completed'; properties: CommonProperties & { duration_bucket: 'lt_1s' | '1s_to_5s' | '5s_to_30s' | '30s_plus' } } + | { event: 'thread.persisted'; properties: CommonProperties & { persistence_kind: 'remote_checkpoint' } } + | { event: 'interrupt.handled'; properties: CommonProperties & { resolution_kind: 'provided' | 'approved' | 'rejected' | 'edited' } } + | { event: 'generative_ui.rendered'; properties: CommonProperties & { renderer: 'json_render' | 'a2ui' } }; +``` + +Unknown top-level keys, event names, properties, enum values, arrays, and client `$` properties are rejected. The server creates `$insert_id`, `$process_person_profile:false`, `$ip:null`, `received_at`, stored opaque `distinctId`, `identity_state`, `verification:'client_reported'`, and `source:'public_runtime'`. It never forwards the claim hash. + +Operational default budgets are versioned configuration: 600 accepted/rejected ingest attempts per HMAC-IP per minute and 120 accepted attempts per project per minute. Tests inject smaller limits. Production can override downward/upward through non-public environment variables after reject-rate review. + +--- + +## Task 1: Prove install and import inertness across every package manager + +**Files:** + +- Create: `tools/inert-install/project.json` +- Create: `tools/inert-install/verify-inert-install.mjs` +- Create: `tools/inert-install/import-probe.mjs` +- Create: `tools/inert-install/trace-parser.mjs` +- Create: `tools/inert-install/trace-parser.spec.mjs` +- Create: `tools/inert-install/fixtures/npm/package.json` +- Create: `tools/inert-install/fixtures/pnpm/package.json` +- Create: `tools/inert-install/fixtures/yarn/package.json` +- Create: `tools/inert-install/fixtures/bun/package.json` +- Create manager lock/config files under those fixture directories +- Modify: `.github/workflows/ci.yml` +- Modify: `scripts/ci-workflow.spec.mjs` +- Reuse/modify only as needed: `libs/telemetry/scripts/assemble-dist.spec.mjs`, `scripts/mit-cutover.spec.mjs` + +- [ ] **Step 1: Test trace parsing red.** Feed representative Linux `strace` lines and require rejection of every AF_INET/AF_INET6 `connect`, including child processes, while Unix-domain connections are allowed. + +- [ ] **Step 2: Add manifest/dependency tests.** Pack all six release packages, inspect packed manifests for `preinstall|install|postinstall|prepare|bin`, and walk packed dependency metadata for Scarf/configured install-analytics packages. + +- [ ] **Step 3: Add import probes.** Stub/trace fetch, HTTP, HTTPS, DNS, sockets, file identity writes, and browser localStorage. Import each public entry point and construct providers/clients without an eligible runtime operation. + +- [ ] **Step 4: Run red.** + +```bash +node --test tools/inert-install/trace-parser.spec.mjs +npx nx run inert-install:test --skip-nx-cache +``` + +Expected: FAIL because the project/fixtures do not exist. + +- [ ] **Step 5: Implement the verifier.** Build tarballs, prewarm manager stores, reset fixture install state, install only local tarballs offline under `strace -f -e trace=network`, and run import probes. The verifier itself must distinguish manager cache activity from package lifecycle execution and fail on any lifecycle-originated network syscall. + +- [ ] **Step 6: Add the Ubuntu CI gate.** Install pinned pnpm, Yarn, Bun, and `strace`; run after package builds. Local Darwin runs parser/manifest/import tests but reports the syscall matrix as CI-only. + +- [ ] **Step 7: Run green where supported.** + +```bash +node --test tools/inert-install/trace-parser.spec.mjs +npx nx run inert-install:test --skip-nx-cache +npx nx run inert-install:verify --skip-nx-cache +``` + +Expected on Linux CI: all four offline installs/import probes complete with zero AF_INET/AF_INET6 connections attributable to package lifecycle execution. + +--- + +## Task 2: Replace the open SDK model with one versioned five-event contract + +**Files:** + +- Create: `libs/telemetry/src/shared/contract.ts` +- Create: `libs/telemetry/src/shared/contract.spec.ts` +- Modify: `libs/telemetry/src/shared/events.ts` +- Modify: `libs/telemetry/src/shared/public-api.ts` +- Modify: `libs/telemetry/src/index.ts` +- Modify: `libs/chat/src/lib/agent/runtime-telemetry.ts` +- Modify associated type/spec files in Chat + +- [ ] **Step 1: Write the five valid-event tests.** Assert the exact common/event-specific shapes and normalization into the wire request. + +- [ ] **Step 2: Write every rejection test.** Unknown event/key/property/enum, cross-event property, array, depth four, malformed UUID/SemVer/time/hash, overlong strings, raw content-like properties, and `$` prefixes. + +- [ ] **Step 3: Run red.** + +```bash +npx nx test telemetry --skip-nx-cache +``` + +Expected: FAIL because arbitrary `tplane:*` events/properties remain valid. + +- [ ] **Step 4: Implement a closed discriminated union and runtime parser.** Remove public construction/request/start/end/error names. Keep closed local error categories out of the growth wire path. + +- [ ] **Step 5: Narrow the Chat sink types.** Preserve a caller-supplied sink for inspection/testing, but it receives only approved milestone payloads. + +- [ ] **Step 6: Run green.** + +```bash +npx nx test telemetry --skip-nx-cache +npx nx test chat --skip-nx-cache +npx nx run chat:type-tests --skip-nx-cache +``` + +Expected: PASS. + +--- + +## Task 3: Implement lazy project/session identity, claim secret, controls, and transport + +**Files:** + +- Create: `libs/telemetry/src/shared/identity.ts` +- Create: `libs/telemetry/src/shared/identity.spec.ts` +- Create: `libs/telemetry/src/shared/claim.ts` +- Create: `libs/telemetry/src/shared/claim.spec.ts` +- Create: `libs/telemetry/src/browser/project-store.ts` +- Create: `libs/telemetry/src/browser/project-store.spec.ts` +- Create: `libs/telemetry/src/node/project-store.ts` +- Create: `libs/telemetry/src/node/project-store.spec.ts` +- Create: `libs/telemetry/src/node/transport.spec.ts` +- Modify: `libs/telemetry/src/shared/env.ts` +- Modify: `libs/telemetry/src/shared/env.spec.ts` +- Modify: `libs/telemetry/src/shared/anon-id.ts` +- Modify: `libs/telemetry/src/node/client.ts` +- Modify: `libs/telemetry/src/node/adapter.ts` +- Modify: `libs/telemetry/src/browser/service.ts` +- Modify: `libs/telemetry/src/browser/tokens.ts` +- Modify: `libs/telemetry/src/browser/browser-silence.spec.ts` +- Modify public entry points and README only as needed for package users; the website privacy plan controls rendered site copy + +- [ ] **Step 1: Test import/construction silence.** No UUID, filesystem, localStorage, fetch, HTTP, DNS, or socket action before the first eligible milestone. + +- [ ] **Step 2: Test first-use identity.** Lazily create a UUID project, one nonpersistent session UUID, and a cryptographically random 32-byte claim secret. Browser persistence is versioned localStorage; Node persistence is an atomic permission-restricted file keyed by SHA-256 of cwd, with no cwd/path in any payload. + +- [ ] **Step 3: Test claim registration retries.** Send the same claim hash on every eligible event until a successful gateway response explicitly acknowledges project registration. Lost response repeats the identical hash; a registration acknowledgment stops it. + +- [ ] **Step 4: Test control precedence.** `telemetry:false`, programmatic disable, `DO_NOT_TRACK=1`, `TPLANE_TELEMETRY_DISABLED=1`, and CI disable before any ID allocation. Invalid explicit ID/endpoint overrides disable capture rather than split identity. + +- [ ] **Step 5: Test debug mode.** `TPLANE_TELEMETRY_DEBUG=1` (and browser programmatic `debug:true`) prints the exact endpoint and payload with secrets omitted, performs no network request, and leaves product behavior unchanged. + +- [ ] **Step 6: Test transport failure isolation.** Timeout, DNS, 400, 429, and 500 never reject or delay the runtime API. Use a short abortable timeout and no unbounded retry queue. + +- [ ] **Step 7: Run red then implement.** + +```bash +npx nx test telemetry --skip-nx-cache +npx nx build telemetry --configuration=production --skip-nx-cache +``` + +Expected after implementation: PASS, and built entry points remain import-inert. + +--- + +## Task 4: Make Chat's milestone sink the default while preserving overrides + +**Files:** + +- Modify: `libs/chat/package.json` +- Modify: `libs/chat/ng-package.json` +- Modify: `package-lock.json` +- Modify: `libs/chat/src/lib/agent/runtime-telemetry.ts` +- Modify: `libs/chat/src/lib/agent/agent.ts` and/or the actual runtime-construction file selected during implementation +- Modify: `libs/chat/src/lib/agent/*.spec.ts` covering resolver precedence +- Modify: `libs/langgraph/src/lib/agent.types.ts` +- Modify: `libs/langgraph/src/lib/agent.provider.ts` +- Modify: `libs/ag-ui/src/lib/provide-agent.ts` + +- [ ] **Step 1: Write resolver precedence tests.** `telemetry:false` disables all sinks; an explicit custom sink wins; otherwise the built-in nonblocking milestone sink is used. Environment/programmatic disables still win inside the built-in sink. + +- [ ] **Step 2: Run red.** + +```bash +npx nx test chat --skip-nx-cache +npx nx run chat:type-tests --skip-nx-cache +``` + +Expected: FAIL because the current default is no sink. + +- [ ] **Step 3: Add the package dependency.** Import only the browser-safe telemetry entry; add `@threadplane/telemetry` to `allowedNonPeerDependencies` so Angular packaging is explicit. Update `package-lock.json` with hunk-level review because it is already dirty in the current worktree. + +- [ ] **Step 4: Install the resolver in each runtime construction path.** Do not create identity during dependency injection or construction. + +- [ ] **Step 5: Update public option descriptions in source without adding website promises.** `telemetry:false` remains the explicit runtime disable. + +- [ ] **Step 6: Run green.** + +```bash +npx nx test chat --skip-nx-cache +npx nx run chat:type-tests --skip-nx-cache +npx nx build chat --configuration=production --skip-nx-cache +``` + +Expected: PASS. + +--- + +## Task 5: Emit LangGraph milestones only at semantic success seams + +**Files:** + +- Modify: `libs/langgraph/src/lib/internals/stream-manager.bridge.ts` +- Modify: `libs/langgraph/src/lib/internals/stream-manager.bridge.spec.ts` +- Modify: `libs/langgraph/src/lib/agent.fn.ts` +- Modify: `libs/langgraph/src/lib/agent.fn.spec.ts` +- Modify: `libs/langgraph/src/lib/agent.types.ts` +- Modify: `libs/langgraph/src/lib/lifecycle.ts` +- Modify: `libs/langgraph/src/lib/lifecycle.spec.ts` + +- [ ] **Step 1: Test connection.** Emit once per runtime session after the first decoded event, immediately before `processEvent(event)`, for owned and joined streams; no construction/request event. The gateway still scores `transport.connected` once per project, but per-session connection facts are required to derive a seven-day return. + +- [ ] **Step 2: Test completion.** Emit once only when `finalizeClosedAttempt(...) === 'success'`, with a closed duration bucket. Abort, error, pause, or interrupt terminal states do not count. + +- [ ] **Step 3: Test persistence.** Initial `refreshHistory(false)` with a nonempty remote checkpoint predating this runtime emits once. General history subscriptions and `refreshHistory(true)` after a run do not. + +- [ ] **Step 4: Test interrupt handling.** Submission alone does not count. After resumed completion succeeds, emit once with a typed resolution-kind hint (default `provided`) and never inspect/transmit the resolution value. + +- [ ] **Step 5: Run red then implement.** + +```bash +npx nx test langgraph --skip-nx-cache +npx nx run langgraph:type-tests --skip-nx-cache +``` + +Expected after implementation: PASS for owned/joined streams and all negative outcomes. + +--- + +## Task 6: Emit AG-UI milestones only from protocol-confirmed success + +**Files:** + +- Modify: `libs/ag-ui/src/lib/to-agent.ts` +- Modify: `libs/ag-ui/src/lib/to-agent.spec.ts` +- Modify: `libs/ag-ui/src/lib/to-agent.resume.spec.ts` +- Modify: `libs/ag-ui/src/lib/to-agent.resume-wire.spec.ts` +- Modify: `libs/ag-ui/src/lib/provide-agent.ts` +- Modify: `libs/ag-ui/src/lib/provide-agent.spec.ts` + +- [ ] **Step 1: Test connection on first `RUN_STARTED`.** It fires once per runtime session and contains no run/thread/provider ID. The gateway's project milestone key, not the SDK latch, prevents repeated score credit. + +- [ ] **Step 2: Test completion on successful `RUN_FINISHED`.** Interrupted `RUN_FINISHED` reduces to paused and does not count; transport close without successful finish does not count. + +- [ ] **Step 3: Make `executeRun` expose/retain its final outcome.** Use that result to emit success-only completion and success-only resume handling. + +- [ ] **Step 4: Test resolution payload exclusion.** Only the approved resolution-kind enum crosses the sink. + +- [ ] **Step 5: Run red then implement.** + +```bash +npx nx test ag-ui --skip-nx-cache +npx nx run ag-ui:type-tests --skip-nx-cache +``` + +Expected after implementation: PASS. + +--- + +## Task 7: Emit generative UI activation only after a real renderer mounts + +**Files:** + +- Modify: `libs/render/src/lib/lifecycle.ts` +- Modify: `libs/render/src/lib/lifecycle.spec.ts` +- Modify: `libs/render/src/lib/render-lifecycle.service.ts` +- Modify: `libs/render/src/lib/render-element.component.ts` +- Modify: `libs/render/src/lib/render-element.component.spec.ts` +- Modify: `libs/render/src/lib/render-spec.component.ts` +- Modify: `libs/render/src/lib/render-spec.component.spec.ts` +- Modify: `libs/chat/src/lib/compositions/chat/chat.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat/chat.component.spec.ts` + +- [ ] **Step 1: Test the negative seam.** Non-null specs and wrapper `ngOnInit` do not emit activation. + +- [ ] **Step 2: Extend the lifecycle with one actual-mount fact.** Emit only after `mountedReal` sees `!notReady()` and a registered `entry()?.component`; latch once. + +- [ ] **Step 3: Translate in Chat.** The central JSON Render/A2UI handlers attach effective runtime sink context and emit `renderer:'json_render'|'a2ui'` without spec/state content. + +- [ ] **Step 4: Run red then implement.** + +```bash +npx nx test render --skip-nx-cache +npx nx test chat --skip-nx-cache +``` + +Expected after implementation: PASS; actual mount counts once, spec presence never counts. + +--- + +## Task 8: Migrate rate limiting to HMAC fixed-window buckets + +**Files:** + +- Create: `migrations/0004_ingest_rate_limit_buckets.sql` +- Modify: `scripts/rate-limit.ts` +- Modify: `scripts/rate-limit.spec.ts` +- Create: `apps/website/src/lib/telemetry/ip-hash.ts` +- Create: `apps/website/src/lib/telemetry/ip-hash.spec.ts` +- Create: `apps/website/src/lib/telemetry/rate-limit.ts` +- Create: `apps/website/src/lib/telemetry/rate-limit.spec.ts` + +- [ ] **Step 1: Write failing generic limiter tests.** Require `scope`, HMAC subject hash, fixed bucket start, atomic increment/upsert, expiry cleanup, injected limits/time, and fail-closed result for analytics. + +- [ ] **Step 2: Preserve existing demo behavior safely.** Migrate the canonical demo caller to the same hashed subject API; product requests retain their current fail-open product policy, while analytics ingest drops/fails closed. + +- [ ] **Step 3: Write the migration.** Convert the ephemeral raw-IP table into generic hashed buckets, removing stored raw IP rows. Keep this operational table outside the five growth CRM tables. + +- [ ] **Step 4: Run red then implement.** + +```bash +npx vitest run scripts/rate-limit.spec.ts apps/website/src/lib/telemetry/ip-hash.spec.ts apps/website/src/lib/telemetry/rate-limit.spec.ts +TEST_DATABASE_URL="$TEST_DATABASE_URL" npm run db:migrate +``` + +Expected after implementation: PASS; no raw IP column/value remains. + +--- + +## Task 9: Harden `/api/ingest`, register projects, and write set-based projections + +**Files:** + +- Create: `apps/website/src/lib/telemetry/ingest-contract.ts` +- Create: `apps/website/src/lib/telemetry/ingest-contract.spec.ts` +- Create: `apps/website/src/lib/telemetry/posthog.ts` +- Create: `apps/website/src/lib/telemetry/project-registration.ts` +- Create: `apps/website/src/lib/telemetry/project-registration.spec.ts` +- Modify: `apps/website/src/app/api/ingest/route.ts` +- Replace/expand: `apps/website/src/app/api/ingest/route.spec.ts` +- Modify: `libs/growth/src/lib/jobs.ts` or add a focused project/activity repository in `libs/growth`; do not create a competing website repository + +- [ ] **Step 1: Test body handling before JSON parsing.** Reject missing/invalid content length, streamed bodies over 8 KiB, malformed JSON, arrays, and depth four with the approved generic public response text. + +- [ ] **Step 2: Test exact schema and server ownership.** Cover every valid event and every malformed/spoofed field. Caller-supplied source/verification/identity/IP/person-profile/reserved properties cannot survive. + +- [ ] **Step 3: Test two budgets.** HMAC-IP before expensive work and project budget after validation/registration. Limiter/storage failure drops the analytics request; SDK still swallows it. + +- [ ] **Step 4: Test first-write project registration.** Unknown project requires claim hash; insert project with stored opaque PostHog distinct ID and claim hash; identical retries acknowledge; conflicting hash rejects and never replaces. + +- [ ] **Step 5: Test idempotent projections without a raw event lake.** Accept `transport.connected` once per session and insert `runtime:session:::transport.connected`; derive `project.returned_7d` from a second distinct session 24 hours to seven days later. For all five events, a separate `runtime:milestone::` key limits score to once per project. Replays/new event IDs cannot add score. Raw request bodies are not persisted. + +- [ ] **Step 6: Test PostHog projection.** Use stored opaque distinct ID, event ID as `$insert_id`, no claim hash/PII, and server-owned normalized fields. Duplicate network forwarding is acceptable; downstream insert ID and Neon set keys make outcomes idempotent. + +- [ ] **Step 7: Run red then implement.** + +```bash +npx nx test website -- --run apps/website/src/lib/telemetry apps/website/src/app/api/ingest/route.spec.ts +TEST_DATABASE_URL="$TEST_DATABASE_URL" npx nx run growth:test-integration +``` + +Expected after implementation: malformed/spoofed/replayed/abusive inputs are rejected, valid events return registration acknowledgment, and product code never sees gateway failures. + +--- + +## Task 10: Implement the explicit one-time project claim and `/connect` + +**Files:** + +- Modify: `libs/telemetry/src/shared/claim.ts` +- Modify: `libs/telemetry/src/shared/claim.spec.ts` +- Create: `apps/website/src/app/connect/page.tsx` +- Create: `apps/website/src/components/connect/ProjectClaimForm.tsx` +- Create: `apps/website/src/components/connect/ProjectClaimForm.spec.tsx` +- Create: `apps/website/src/app/api/growth/projects/claim/route.ts` +- Create: `apps/website/src/app/api/growth/projects/claim/route.spec.ts` +- Modify: `apps/website/e2e/public-copy.spec.ts` +- Extend: `libs/growth/src/lib/contacts.ts`/project repository with the post-proof link-and-approve transaction + +- [ ] **Step 1: Test the local URL API.** Before identity exists, `getThreadplaneProjectClaimUrl()` returns no URL. After first eligible operation it returns `https://threadplane.ai/connect#project_id=&claim_secret=`, without opening, logging, analytics capture, or automatic transmission. + +- [ ] **Step 2: Test fragment handling.** The client reads the fragment and calls `history.replaceState` immediately before navigation/analytics/rendered form activity. The raw secret never enters query params, server logs, referrers, or PostHog. + +- [ ] **Step 3: Test proof.** Hash the submitted secret; constant-time compare to the first-write stored hash; atomically require `claim_consumed_at is null`; reject UUID-only, wrong, conflicting, replayed, or consumed claims uniformly. + +- [ ] **Step 4: Test identity transition.** On proof success, upsert/link the contact, record `project.claimed` and exact visible connect notice, set approval only through the lifecycle plan's approval transaction, consume the claim once, and enqueue the standard lifecycle jobs. Prior runtime events remain `client_reported`. + +- [ ] **Step 4a: Extend the permanent public-output boundary.** Add `/connect` to the non-indexed-route crawl and assert its HTML and public claim responses contain no blocked website term from the privacy plan. + +- [ ] **Step 5: Run red then implement.** + +```bash +npx nx test telemetry -- --run libs/telemetry/src/shared/claim.spec.ts +npx nx test website -- --run apps/website/src/components/connect/ProjectClaimForm.spec.tsx apps/website/src/app/api/growth/projects/claim/route.spec.ts +TEST_DATABASE_URL="$TEST_DATABASE_URL" npx nx run growth:test-integration +``` + +Expected after implementation: PASS. + +--- + +## Task 11: Close website acquisition analytics and PostHog identity behavior + +**Files:** + +- Modify: `apps/website/instrumentation-client.ts` +- Modify: `apps/website/src/lib/analytics/events.ts` +- Create: `apps/website/src/lib/analytics/acquisition-contract.ts` +- Create: `apps/website/src/lib/analytics/acquisition-contract.spec.ts` +- Create: `apps/website/src/lib/analytics/content-registry.ts` +- Create: `apps/website/src/components/analytics/AnalyticsPageview.tsx` +- Create: `apps/website/src/components/analytics/AnalyticsPageview.spec.tsx` +- Create: `apps/website/src/components/analytics/ContentEngagement.tsx` +- Create: `apps/website/src/components/analytics/ContentEngagement.spec.tsx` +- Modify: `apps/website/src/lib/analytics/client.ts` +- Modify: `apps/website/src/lib/analytics/server.ts` +- Modify: `apps/website/src/lib/analytics/server.spec.ts` +- Modify: `apps/website/src/components/docs/CopyButton.tsx` +- Modify CopyButton and form component tests +- Modify: `apps/website/src/lib/analytics/ai-traffic.ts` +- Modify: `apps/website/src/lib/analytics/ai-traffic.spec.ts` +- Modify: `apps/website/src/middleware.ts` +- Modify middleware tests + +- [ ] **Step 1: Test explicit PostHog init.** Require `person_profiles:'identified_only'`, `autocapture:false`, `disable_session_recording:true`, `capture_pageview:false`, `capture_pageleave:false`, `persistence:'memory'`, and DNT respect. + +- [ ] **Step 2: Test pathname-only pageviews and a closed acquisition union.** Retain `$pageview`, CTA click, form submit, newsletter submit, whitepaper submit, registered content engagement, and annotated install-command copy. Strip query/fragment; remove open property index signatures. + +- [ ] **Step 3: Test engagement semantics.** Registered content only, 30 active foreground seconds plus 50% scroll, once per session/content. Keep session ID short-lived/in-memory. + +- [ ] **Step 4: Test install-copy semantics.** Only code blocks explicitly annotated with a published package and `npm|pnpm|yarn|bun` emit `docs:install_command_copied`; general copy does not. + +- [ ] **Step 5: Remove noisy events.** Retire client success/failure, docs search strings/clicks, tabs/sidebar, blog/general copy, destination URLs, CTA text, and raw error reasons. Server persistence emits form acceptance. + +- [ ] **Step 6: Remove server PII projections.** Delete deterministic email IDs and email/name/domain/company PostHog properties. Emit only a random contact projection after Neon persistence when a server business event needs it. + +- [ ] **Step 7: Close crawler/referrer analytics.** Retain these as non-scoring operational events only. Map crawler tokens to `openai|anthropic|perplexity|google|apple|bytedance|meta|common_crawl`; map answer-engine referrers to `chatgpt|perplexity|claude|gemini|copilot|you`; send pathname only and never raw user-agent/referrer. + +- [ ] **Step 8: Run red then implement.** + +```bash +npx nx test website -- --run apps/website/src/lib/analytics apps/website/src/components/analytics +npx nx test website +npx nx build website --configuration=production --skip-nx-cache +``` + +Expected after implementation: PASS; automated scans/mocks see no raw email/name/company/hash/query/fragment/user-agent in PostHog calls. + +--- + +## Task 12: Replace PostHog contracts, dashboards, and rollout controls + +**Files:** + +- Modify: `tools/posthog/telemetry-contract.ts` +- Modify: `tools/posthog/telemetry-contract.spec.ts` +- Modify: `tools/posthog/taxonomy.spec.ts` +- Modify: `tools/posthog/schema.ts` +- Modify: `tools/posthog/schema.spec.ts` +- Modify: `tools/posthog/dashboards/developer-funnel.json` (sole cross-plan dashboard owner; consume lifecycle-exported campaign outcome definitions without letting the lifecycle plan edit this file) +- Replace/remove obsolete `tools/posthog/dashboards/runtime-telemetry.json` +- Replace/remove old runtime insight JSON files with five milestone/activation definitions +- Modify: `docs/gtm/taxonomy.md` + +- [ ] **Step 1: Test the new source/trust split.** Public runtime events are always client-reported; server events use the internal capture path; acquisition events cannot masquerade as activation. + +- [ ] **Step 2: Replace old dashboards/insights.** Use the five milestones, return-within-seven-days, acquisition depth, and aggregate campaign outcomes. Keep public/server verification visible. Do not add open/click metrics. + +- [ ] **Step 3: Add reject/abuse operational definitions.** Monitor gateway reject rate, IP/project budget exhaustion, conflicting project hashes, event-ID collisions observed downstream, unusual event distribution, and cardinality growth. + +- [ ] **Step 4: Run local contract verification.** + +```bash +npx nx test posthog-tools --skip-nx-cache +npm run posthog:sync +``` + +Expected: tests pass and sync plan shows only intended definitions. Do not run `npm run posthog:apply` during implementation. + +--- + +## Task 13: Full verification and staged rollout + +**Files:** + +- Modify: `.github/workflows/ci.yml` and scope tests as needed to select inert-install and changed runtime surfaces +- Create: `docs/superpowers/runbooks/2026-08-31-runtime-analytics-rollout.md` + +- [ ] **Step 1: Run the complete local project surface.** + +```bash +npx nx run-many -t test --projects=telemetry,chat,langgraph,ag-ui,render,website,posthog-tools --skip-nx-cache +npx nx run-many -t lint --projects=telemetry,chat,langgraph,ag-ui,render,website,posthog-tools --skip-nx-cache +npx nx run chat:type-tests --skip-nx-cache +npx nx run langgraph:type-tests --skip-nx-cache +npx nx run ag-ui:type-tests --skip-nx-cache +npx nx run-many -t build --projects=telemetry,chat,langgraph,ag-ui,render --configuration=production --skip-nx-cache +npx nx build website --configuration=production --skip-nx-cache +node --test scripts/ci-scope.spec.mjs scripts/ci-workflow.spec.mjs +git diff --check +``` + +Expected: all pass. + +- [ ] **Step 2: Run the Linux-only install gate in CI.** + +```bash +npx nx run inert-install:verify --skip-nx-cache +``` + +Expected: all four managers report zero lifecycle network syscalls. + +- [ ] **Step 3: Deploy strict ingest dark.** Accept no old open events; validate/reject and observe rates before publishing the SDK default. Keep failures silent to product callers. + +- [ ] **Step 4: Deploy `/connect` and privacy-safe website PostHog settings.** Verify claim fragment clearing and one-time consumption with test projects. + +- [ ] **Step 5: Deploy the canonical privacy-policy plan before default-on runtime capture.** Crawl production using that plan's output boundary. + +- [ ] **Step 6: Publish runtime instrumentation gradually.** Shadow validation, sampled/test projects, then default-on eligible operations. Construction/import/install remain inert; opt-out/debug are verified in packed consumer fixtures. + +- [ ] **Step 7: Apply PostHog definitions only after reviewing the sync plan.** Keep old dashboards during a short comparison window but stop producing old client events in the new release. + +- [ ] **Step 8: Tune budgets from observed reject distribution.** Document any default change and keep tests configuration-driven. + +- [ ] **Step 9: Create one logical commit after full verification.** Repository guidance forbids mid-task commits. Re-record `git status --short`, inspect `git diff -- `, and stage only those paths/hunks; never use a broad `git add apps/website`, `git add .`, or lockfile overwrite. + +--- + +## Acceptance checklist + +- All published packages install/import with no lifecycle network call or identity write. +- First eligible operation lazily creates stable project/claim identity and a nonpersistent session; explicit controls prevent even that allocation. +- Exactly five allowlisted milestones exist, and semantic tests prove their success conditions. +- `/api/ingest` enforces byte/depth/schema/time/idempotency/rate/claim rules and labels every public fact client-reported. +- Neon stores only project/contact/business projections, not raw product payloads; PostHog receives no PII or claim secret. +- Project UUID alone cannot link identity; one-time possession proof plus visible approval is required. +- Website PostHog is explicit-only, memory-persistent, identified-profile-only, and query/fragment/user-agent safe. +- Anonymous signals can score projects but cannot select an individual or authorize outreach.