Skip to content

chore(deps): Bump fast-uri from 3.1.0 to 3.1.2 in /packages/mcp - #13

Closed
dependabot[bot] wants to merge 387 commits into
mainfrom
dependabot/npm_and_yarn/packages/mcp/fast-uri-3.1.2
Closed

chore(deps): Bump fast-uri from 3.1.0 to 3.1.2 in /packages/mcp#13
dependabot[bot] wants to merge 387 commits into
mainfrom
dependabot/npm_and_yarn/packages/mcp/fast-uri-3.1.2

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github May 28, 2026

Copy link
Copy Markdown

Bumps fast-uri from 3.1.0 to 3.1.2.

Release notes

Sourced from fast-uri's releases.

v3.1.2

⚠️ Security Release

What's Changed

Full Changelog: fastify/fast-uri@v3.1.1...v3.1.2

v3.1.1

⚠️ Security Release

What's Changed

New Contributors

Full Changelog: fastify/fast-uri@v3.1.0...v3.1.1

Commits
  • 919dd8e Bumped v3.1.2
  • c65ba57 fixup: linting
  • 6c86c17 Merge commit from fork
  • a95158a Handle malformed fragment decoding without throwing (#171)
  • cea547c Bumped v3.1.1
  • 876ce79 Merge commit from fork
  • dcdf690 ci: add lock-threads workflow (#169)
  • c860e65 build(deps-dev): bump neostandard from 0.12.2 to 0.13.0 (#167)
  • 9b4c6dc build(deps): bump fastify/workflows/.github/workflows/plugins-ci.yml (#166)
  • 85d09a9 build(deps): bump fastify/workflows/.github/workflows/plugins-ci-package-mana...
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    You can disable automated security fix PRs for this repo from the Security Alerts page.

seungdo-keum and others added 30 commits April 28, 2026 13:53
…e pattern signals

Reference-repo benchmarking research (hermes-webui + ComposioHQ + Harvey
patterns) flagged binary approve/reject as the lowest-hanging
learning-loop improvement. Replaced with a 4-tier sub-action that
captures operator INTENT, not just verdict:

  once     : approve / reject as today (default). +0.05 / -0.20 deltas.
  session  : approve + apply for the rest of operator's session.
             Same delta as once; the session memo is the differentiator.
  promote  : approve + force-promote the rule to confidence ≥ 0.85.
             Snaps above the T4 auto-apply threshold immediately so
             future agent runs treat it as a default ("APPLY this
             template" framing), not a soft hint.
  unsafe   : reject + flag the rule as do-not-produce for 30 days.
             Confidence drops to 0 + bannedUntil written so T4 surfaces
             a strong "DO NOT produce this" warning to the model.

Wire-up
  - PATCH /api/approval-queue/[id] body now optionally accepts
    `subAction: "once" | "session" | "promote" | "unsafe"`. Cross-
    validation rejects incompatible combos (promote ⇄ approve only,
    unsafe ⇄ reject only, etc.) with 400.
  - AuditLog details now stamps `subAction` so calibration analysis
    can split "approved-promoted" (verdict=1, deliberate) from
    "approved-once" (verdict=1, rubber-stamp). Both have same brier
    weight but different intent.
  - recordApprovalLearning(opts.subAction) threads through to:
    - new rule on promote: confidence=0.85 + promoted: true
    - new rule on unsafe: confidence=0 + bannedUntil = now+30d
    - existing rule on promote: snap confidence ≥ 0.85
    - existing rule on unsafe: confidence=0 + bannedUntil written
  - T4 firm-patterns reader now renders three sections: "APPLY
    (promoted)" / "Consider (candidate)" / "DO NOT produce (rejected
    unsafe by operator within last 30 days)". Unsafe rules carry an
    explicit negative framing instead of being silently dropped.
  - Outcome enum extended: "promoted" | "banned" added alongside
    "new" | "reinforced" | "weakened" | "skipped".

The change is fully backward-compatible — existing approve/reject
calls without subAction work exactly as before. Operators who haven't
adopted the tiered UI keep their current learning behavior.

221/221 vitest pass · type-check ✅. UI surface for the new buttons
is a follow-up (the API + learning loop is the substantive part —
the UI layer is mechanical once we have a Figma).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on sameAs + auto-id headings

The four highest-leverage AEO/GEO surface upgrades from the
research's top-12 list, all on the rendered blog page.

1. KeyTakeaways component (src/components/blog/key-takeaways.tsx)
   - Renders authored `post.keyTakeaways` as a numbered TL;DR card
     above the body. Per Averi data, standalone summary blocks at
     the top of an article get quoted directly +30-40% more often
     than synthesized prose. <aside role="note"> + Sparkles icon +
     dashboard's zinc dark palette.
   - Mirrors `Article.abstract` (extended in src/lib/seo/json-ld.tsx)
     so the rendered HTML and structured data carry the same
     standalone summary.

2. AuthorBio component + Person JSON-LD (src/components/blog/author-bio.tsx)
   - Visible author bio card at the bottom of every post: name +
     credential + bio + avatar fallback (gradient initials) +
     verified `sameAs` cross-platform handles.
   - `authorPersonJsonLd()` emits the same data as a Person entity
     paired with the Article so AI engines build the author-authority
     graph across our corpus. Per AEO research, author authority is
     one of Claude's strongest citation signals.
   - AUTHORS map starts with the founder; sameAs intentionally empty
     until LinkedIn/X handles are verified — fake sameAs is a
     manual-action risk per Google's policy.
   - Compact byline mode for the article header (future migration);
     full card mode for the bottom of the post.

3. Article.abstract in JSON-LD (src/lib/seo/json-ld.tsx)
   - When `post.keyTakeaways` is authored, joins them as a single
     paragraph and emits `Article.abstract`. Falls back to
     `ogDescription` otherwise.
   - Also reads `post.dateModified` for `Article.dateModified` so
     AI engines see freshness when a post is materially updated
     (default falls back to publish date).

4. Auto-id headings (src/app/blog/[slug]/page.tsx::addHeadingIds)
   - Slugifies every <h2>/<h3>/<h4> and adds id="<slug>" so AI
     engines can deep-link to a specific section
     (`page.com#why-clients`). Idempotent (skips headings with an
     explicit id), disambiguates duplicates (`-2`, `-3`).
   - Pure helper + 12-test unit suite covering: slugify shape,
     leading/trailing trim, multi-space collapse, 80-char cap,
     idempotency, h3/h4 inclusion, dup disambiguation, nested HTML,
     empty-text skip.

233/233 vitest pass · type-check ✅ · lint ✅. Backward compatible —
posts without `keyTakeaways` skip the TL;DR card and the abstract
falls back to ogDescription. The AuthorBio block renders for every
post with an existing AUTHORS map entry; unknown authors get a
graceful generic credential.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…first lint

Closes the RUN 22 AEO/GEO advanced wave with two more research-backed
upgrades.

1. HowTo schema auto-emission (src/lib/seo/json-ld.tsx)
   - extractHowToSteps() walks rendered HTML and detects every <h2>
     starting with "Step N:" / "1." / "2." patterns. Pulls the
     paragraph that follows as the step text.
   - howToJsonLd() emits a schema.org HowTo entity with every step's
     position + name + text. Only fires when ≥ 3 steps are detected
     (Google's rich-result threshold; partial HowTo throws "missing
     required field" warnings).
   - Wired into src/app/blog/[slug]/page.tsx alongside Article + FAQ
     + Person JSON-LD. Existing posts without step-shaped headings
     emit nothing extra; posts with workflow procedures suddenly get
     AI Overviews + Perplexity rich-result eligibility.

2. Definition-first lint (scripts/aeo/definition-lint.ts +
   `npm run aeo:lint`)
   - Per the CMU GEO research, the first 150-200 tokens of a page
     carry disproportionate weight in LLM summarization. Articles
     that don't mention the brand-defining sentence in the lede
     ("Practiq is the X workspace for Y") get cited less often
     because LLMs can't tie the page back to the brand entity.
   - Lint walks every blog post + research dataset abstract, takes
     the first ~800 chars (200 tokens), and flags entries missing
     the definition pattern. Returns exit code 1 on miss so CI can
     gate publishes.
   - Initial scan result: 167 of 168 surfaces fail. This is
     ACTIONABLE editorial signal — the team's content style
     intentionally avoids brand mentions in the lede; the AEO
     research says that's a known anti-pattern. Editors can fix
     post-by-post, or accept the failure as a deliberate choice.

233/233 vitest pass · type-check ✅ · lint ✅. The definition-lint
itself is a non-test script (no vitest needed — the runtime exit
code is the contract).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t + auto-id + JSON-LD + admin metrics + tiered approval validation)

13 new Playwright tests covering the agent-pipeline business logic
that the existing smoke spec didn't reach. All 12 active tests pass
against production (the 13th — admin metrics shape — gracefully
skips on the marketing host since it's gated to admin.grindworks.ai).

Coverage:
- a01-a04: cron route auth (3 routes × 401 anonymous + bearer-token
  rejection). Catches the kind of inverted-secret-check bug the
  audit found in seo-submit (RUN 24 #3).
- a05: /llms.txt MIME + section structure
- a06: /llms-full.txt MIME + 7 long-form sections + size floor
  + CC BY 4.0 footer
- a07: /blog/<slug>.md frontmatter carries RUN 22 fields
  (date_published, date_modified, last_verified, license)
- a08: /research/<slug>.md frontmatter (Dataset shape + license)
- a09: blog HTML emits slug-shaped id attrs on <h2> headings
  (RUN 22 Phase 2 auto-id)
- a10: blog page emits Article + Person + BreadcrumbList JSON-LD
  (RUN 22 Phase 2 author authority)
- a11: /api/admin/metrics 404 on marketing host (RUN 19 host gate)
- a12: approval-queue PATCH rejects invalid subAction with 400/401
  (RUN 21 tiered approval validation)
- a13: /api/admin/metrics Prometheus shape (when host permits;
  skips on practiq.dev)

Run results against production deploy (h186yosqk):
  12 passed · 1 skipped · 0 failed · 14s wall clock

Manual deploy to practiq.dev because Vercel auto-deploy is lagging;
RUN 19 + 21 + 22 Phase 1 + 2 + 3 are now all live and verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…page + extended E2E

Three additive surfaces, all read-only / no risk to existing flows.

1. **FoundingCounter** (`src/components/founding-counter.tsx`)
   - Reads the singleton `FoundingSlot` row (cap default 50) and
     renders "X of 50 claimed" + progress bar + low-slots warning.
   - Two variants: `hero` (stacked card) for landing pages, `inline`
     (compact pill) for nav / sticky footer reuse.
   - DB blip fallback: renders "Limited to 50 firms · founding cohort"
     instead of "0 of 50" so the public page never shows a confusing
     zero-claimed scarcity number pre-launch.
   - Mounted on `/founding-member` (hero) AND `/pricing` (hero,
     beneath the existing rounded badge). Both pages set
     `dynamic = "force-dynamic"` + `revalidate = 0` so the counter
     reflects every Stripe-webhook write within seconds.
   - Side-effect safety: this component is READ-ONLY. The Stripe
     webhook holds the only write path with the atomic `UPDATE …
     WHERE claimed_count < cap` pattern. Re-rendering the counter
     a million times produces zero writes.

2. **/app/settings/learned-patterns** (`src/app/app/settings/learned-patterns/page.tsx`)
   - Operator-facing read view of every AgentRule the pattern learner
     has accumulated. Bucketed identically to the T4 firm-patterns
     reader: APPLY (promoted, ≥0.85) / Consider (candidate, 0.6-0.85) /
     DO NOT produce (RUN 21 unsafe within 30d) / Below threshold.
   - Each rule renders: title pattern · item type · client name ·
     confidence + visual bar · times applied · first-seen date.
     `promoted` and `unsafe` flags get colored badges.
   - Empty state nudges the operator to the approval queue.
   - Closes audit finding #15: "skill-generator writes patterns;
     promised UI doesn't exist." It does now — and matches the
     operator's mental model from the AI's perspective (same
     buckets the model sees in T4 firm-patterns prompt).

3. **Extended agent-pipeline E2E** (`tests/e2e/agent-pipeline.spec.ts`)
   - 4 new tests: a14 (download 401 anonymous), a15 (download
     unknown-format 400), a16 (/pricing renders counter), a17
     (/founding-member renders counter + Offer JSON-LD with
     LimitedAvailability).
   - All four explicitly tolerate the cap-only fallback so a
     pre-seeded DB doesn't fail the suite.

233/233 vitest pass · type-check ✅ · lint ✅. New surfaces are
backward-compatible and don't touch any existing write path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…efresh crons

Two new daily/weekly crons that close audit gap #8 (FactEdge writes
absent) and AEO research priority #6 (freshness refresh boosts
~2.5× citation rate).

1. **FactEdge inference** (`/api/cron/factedge-inference`, 04:00 UTC daily)
   - `inferEdgesForClient(clientId)` walks up to 30 active ClientFact
     rows per client, asks Claude (tool_use schema) to propose
     directed edges with one of 5 relations: causes / contradicts /
     refines / depends_on / follows.
   - Cost-bounded: skips clients with < 5 active facts. One Claude
     call per client. ~$0.50/day across ~10 firms × 5 clients.
   - Idempotent: writes via the existing
     `@@unique([fromFactId, toFactId, relation])` constraint; on
     conflict refreshes weight if higher. Re-running is safe.
   - Edge weight floor 0.55 — proposed edges below that drop. Match
     to the corpus's exact fact ids (no invented ids).
   - Slack agent_cron_summary / _warning emits aggregate counters
     at the end. Failure-per-client doesn't kill the cron.

2. **30-day freshness refresh** (`/api/cron/freshness-refresh`, 09:00 UTC Tuesday)
   - Picks up to 6 oldest blog posts whose `dateModified` is > 30
     days ago. Per post, asks Claude to spot ONE stale claim + propose
     a replacement (verbatim phrase to update + 1-3 sentence proposed
     update + rationale + confidence).
   - Persists proposals to `AuditLog` with `action="content_freshness_proposal"`.
     Operator reviews via the audit table or a future admin page.
     **No automated PR creation** — the operator must validate +
     manually apply, which keeps the cron Vercel-runtime-safe (no
     gh credentials required).
   - 14-day cooldown per slug so a previously-proposed post doesn't
     get a duplicate proposal next Tuesday.
   - Confidence < 0.6 → skip. Most posts on most weeks have nothing
     to refresh; empty answer is correct most of the time.
   - Cost: ~$0.12/week (6 posts × ~$0.02 each at Sonnet 4.5).

Safety guarantees:
- Both crons are READ-mostly. Only writes are FactEdge rows (existing
  schema + unique constraint) and AuditLog rows (existing model).
- Both use the existing CRON_SECRET / SEO_DEPLOY_SECRET / x-vercel-cron
  auth path that all other crons share.
- Both fail closed on per-item errors so one bad client / post
  doesn't cascade.
- vercel.json gets 2 new entries (Hobby-compliant: daily + weekly,
  not hourly).

233/233 vitest pass · type-check ✅. New crons additive — zero
behavior change to existing daily-briefing / anomaly-detector /
comms-drafter / digest-compactor flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ddit research

Adds a 6th HeroVariant in the existing A/B test surface, drawn from
the expanded Reddit research wave's top-3 product implications:

  1. **Lock-in fear** (r/legaltech 1mhwrv0 — "Locking into any
     contract with a new tech is a very scary thing.")
  2. **Boutique-only positioning** (r/legaltech 1mhndz0 — Harvey
     explicitly not scheduling demos for small firms.)
  3. **Show-your-work / refusal-first** (r/legaltech 1o4n70h —
     "If you can't show diffs/provenance + hours saved on your
     corpus, pass.")

Headline: "AI for your firm — without the year-long contract."
Subhead: explicitly contrasts Harvey's 12-month + 40-seat minimum
+ promises every answer cites the exact client memory it came from.
Closes with the AEO-friendly "Practiq is the AI workspace for
boutique professional service firms." definition lede so the
RUN 22 Phase 3 definition-lint passes for this variant's HTML render.

Side-effect safety: this is a PURE additive variant. Default cookie
value remains `control`, so existing visitors see no change. The
variant is opt-in via the same `ab_hero_copy_v1` cookie path the
existing 5 variants use.

233/233 vitest pass · type-check ✅. No middleware setter changes
(the existing variants are also reader-only — assignment is manual
or future-traffic-split work).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…final wave-progress

- agent-pipeline.spec.ts: relaxed a16 + a17 founding-counter text
  matcher to "Founding cohort" so it passes both the live path
  ("/ 50 claimed") and the cap-only fallback ("Limited to 50 firms
  · founding cohort"). 16 passed · 1 skipped · 0 failed against
  production deploy `dvp4w87tq`.

- wave-progress.md: appended four sections covering Batch A
  (founding counter / learned-patterns / E2E), Batch B (FactEdge
  inference + freshness refresh crons), Batch D (monthly_no_lockin
  Reddit hero variant), Batch C migration plan reference + a final
  consolidated batch table.

Side-effect verification:
- Founding counter renders correctly on both /pricing and
  /founding-member on production (singleton row exists; live
  variant fires).
- All cron auth paths still 401 anonymous (a01-a04 green).
- LLM benchmark / embedding / structured output all unchanged
  (233/233 vitest).
- No write paths touched outside the Stripe-webhook-only
  FoundingSlot increment + AuditLog/AgentRule/FactEdge appends
  that already had unique-constraint guards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, model picker, conversion devices

Production QA pass against https://practiq.dev surfaced eight defects
that the user explicitly called out plus a few adjacent issues:

- Industry-card navigation on / pushed to /dashboard which 404'd. The
  canonical demo route is /build-dashboard. Updated handleEnterFirm +
  handleTourAllIndustries to point at the correct route, and added a
  308 redirect from /dashboard -> /build-dashboard in next.config so
  bookmarks and old screenshots keep resolving.
- Sign in link was hidden below sm: in the nav, leaving mobile
  visitors no obvious path back to their account. Made the affordance
  visible at every viewport and added an Account column to the footer
  with sign-in / start-trial / live-demo / open-workspace / reset
  password.
- Browser frame on the hero showed practiq.dev/dashboard, now
  practiq.dev/app to match the actual workspace URL.

LLM model selector (user-facing):

- Added preferredModel column on User (nullable, defaults to
  DEFAULT_MODEL_ID at request time).
- Created src/lib/llm/models.ts with a 4-entry catalog (Haiku 4.5,
  Sonnet 4.5, Opus 4.1, GPT-4o) including plan-gate metadata, tier
  labels, and provider-specific id resolution for Anthropic-direct
  vs OpenRouter routing.
- /api/users/me PATCH now validates preferredModel against the user's
  current plan (free/solo/practice/firm) and 403s on max-tier picks
  while on free.
- Settings -> Agent tab renders the picker with locked badges on
  out-of-plan options and a Practice upgrade CTA for trial users.

Free-trial -> paid conversion devices:

- Built PlanUsageMeter — inline meter on /app showing client and chat
  caps with color escalation (zinc -> amber -> red), an UTM-tagged
  upgrade CTA, and auto-hide for paid users below 60% of every cap.
- Wired it into /app/page.tsx with usage queried over the Stripe
  current period (or signup date for trial users).
- Existing TrialCountdownBanner stays in /app/layout.tsx.

Schema sync on deploy:

- Added scripts/db-sync-on-deploy.mjs that runs prisma db push as the
  first build step when DATABASE_URL is set. Ensures the new
  preferred_model column lands in production before the new code is
  served. Honors SKIP_DB_SYNC=1 for frontend-only redeploys.

Blog content (4 new April 2026 posts):

- choosing-llm-model-for-client-work — decision tree for picking
  between Haiku / Sonnet / Opus / GPT-4o, plus the implementation in
  Practiq.
- fourteen-day-trial-reality-check — day-by-day plan a boutique firm
  should run during the free trial.
- client-scoped-ai-memory-vs-chatgpt — why ChatGPT forgets your
  client and how Practiq's per-workspace memory architecture works.
- stripe-checkout-for-boutique-firms — what a working SaaS Stripe
  integration looks like and how to verify a vendor's billing.

Test coverage:

- tests/e2e/post-qa-fixes.spec.ts — 11 assertions covering industry
  card navigation, /dashboard 308 redirect, sign-in visibility at
  small + desktop viewports, footer Account column, /login + /signup
  forms, pricing CTA -> signup redirect for anonymous users, all 4
  new blog posts rendering, /demo redirect into build-dashboard.

Verification:
- npm run type-check passes (zero errors).
- All Stripe price IDs already set on Vercel production
  (STRIPE_PRICE_SOLO/PRACTICE/PRACTICE_FOUNDING/PRACTICE_SEAT/FIRM/
  FIRM_SEAT confirmed via vercel env ls production).
- RESEND_API_KEY, EMAIL_FROM, RESEND_FROM_EMAIL all set.
- NEXTAUTH_URL, NEXTAUTH_SECRET, GOOGLE/LINKEDIN OAuth credentials
  all set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ompat)

Prisma 7's `db push` does NOT accept --skip-generate; passing it drops
the command into the help screen and exits 1. The kles9sf9v build on
2026-04-28 silently failed schema sync because of this, so the
preferred_model column added in ecb8f15 never landed in production —
authenticated /app/settings calls would 500 the moment a real user
hit them.

Fix: drop the flag. Per `prisma db push --help`, the supported flags
are --accept-data-loss and --force-reset only. Client generation runs
separately via the project's postinstall hook so we don't need to
suppress it here.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…te to Day 20

db-sync-on-deploy.mjs (continued):
- Add 90-second timeout to `prisma db push`. The previous (un-timed)
  invocation in deploy van4ufeef hung 18+ minutes waiting on the
  Supabase pgbouncer-side DDL lock for ALTER TABLE practiq.users
  (adding preferred_model). Without a cap any future schema-lock
  contention could burn the entire Vercel build budget.
- On timeout: log the wedged state, continue the build (frontend
  ships), and ask the operator to run `prisma db push` manually
  once the lock clears. Better to deploy a partially-stale schema
  with a clear log line than to deadlock the whole release.
- Distinguish between non-zero exit (real failure) and SIGTERM /
  ETIMEDOUT (lock contention) in the warning copy so post-mortem
  reading the Vercel log knows which lever to pull.

Also in this commit:
- Delete dead /api/documents/route.ts (unimplemented placeholder
  with no callers — the real generate_document handler lives in
  src/lib/claude/tool-handlers.ts and is fully wired).
- Add tests/e2e/persona-journey.spec.ts — authenticated 11-step
  flow that creates a fresh disposable test account, walks signup
  → /app → every Settings tab (incl. Agent model picker) → every
  pricing CTA, and asserts Stripe Checkout is reached. This catches
  the schema-sync regression class for any future DB column.
- Refresh cycle state for the 4/28-29 window:
  - .cycle/events.jsonl: append seq 74-83 (lovable mark verification,
    post-lovable batches A/B/D, deferred plan C, defect cluster
    reported by operator, ecb8f15 fixes shipped, db-sync regression
    found, worktree split + this fix shipped)
  - .cycle/BOARD.md: prepend Day 19-20 narrative + risk register +
    Day 21 plan
  - .cycle/metrics.json: bump to Day 20 (was Day 8 / 4/16); add
    signal_target_v2 (design_partner_conversations / paying /
    signups) thresholds; preserve the retired passive-engine target
    as signal_target_legacy_passive_engine for audit trail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… — every authed checkout 503'd

Operator-reported defect on 2026-04-28 ("결제가 실제 서비스에서 정상적으로
붙어있지 않습니다") had its root cause in src/lib/stripe/client.ts:
isStripeConfigured() was still checking STRIPE_PRICE_STARTER /
STRIPE_PRICE_TEAM / STRIPE_PRICE_PRO — the names from the original
plan tier vocabulary that we replaced with solo / practice / firm
when src/lib/stripe/plans.ts was rebuilt. None of starter/team/pro
have ever been set in production env, so the helper returned false
on every call → /api/stripe/checkout short-circuited to 503 → the
client modal opened with "Billing not configured" instead of redirecting
to Stripe Checkout.

Yesterday's 14/14 E2E pass missed this because the suite drove an
*anonymous* user through /pricing — anon users get 401 (which is
the correct fallback to /signup), masking the 503 for authed
visitors. The new persona-journey.spec.ts pre-provisions a user
via the Supabase Management API and exercises the authed path; that
caught the 503 in the second run.

Fix:
- isStripeConfigured() now matches plans.ts (SOLO / PRACTICE / FIRM).
- Long comment so the next renamer remembers to update the check.
- persona-journey.spec.ts updated to provision the test user via
  pgcrypto's gen_random_uuid() (Prisma's @default(uuid()) isn't a
  Postgres-side default), ON CONFLICT update for re-runnability,
  and to switch to UI-form login (/signup is rate-limited at 5/IP/hr
  and the suite was tripping it).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e Co demo

Adds a POST handler to the existing /api/onboarding/sample route. It
calls seedSampleClient (already idempotent) and returns the sample
client id along with context + approval-item counts. Three callers
benefit:

1. Operators who DELETEd the sample but want it back as a teaching
   surface for a teammate or a design partner walkthrough.
2. The persona-journey E2E that provisions test users via SQL INSERT
   (rate-limit avoidance) — those users skip signup's auto-seed, so
   the spec now POSTs here right after login to populate the workspace
   for the workspace + chat tests.
3. Future "demo mode" UI that lets a logged-in operator reset the
   sample workspace to its pristine state for a recorded demo.

The handler is auth-gated like its GET / DELETE siblings. Errors
swallow into a 500 with a server-side console log; the underlying
seed library never throws on the happy path so this should be safe
for production traffic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e moment green on prod

The persona-journey suite now covers end-to-end: SQL-provisioned user
→ UI login → /app home with PlanUsageMeter → every Settings tab incl.
preferred-model picker save round-tripping through PATCH /api/users/me
→ Acme Coffee Co sample workspace navigation → Chat tab composer
sending a real prompt → streamed /api/chat response landing assistant
text in the transcript → /pricing CTAs for all three plan tiers
reaching checkout.stripe.com.

Two new tests:
  p14  open Acme workspace + switch to Chat tab
  p15  chat send → /api/chat 200 + assistant text rendered

cycle events seq 88-89 record the new endpoint + the green run.

Verified on production deploy 5am2gz8mp at https://practiq.dev.
Run time ~36s. Zero flake observed across 3 consecutive runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…le): refresh metrics with real Day 20 figures

Auth UX:
- Both pages now render an Eye / EyeOff button absolutely positioned
  at the right of the password input. Click flips type='password'↔
  'text'. aria-label switches "Show password"↔"Hide password".
- Tiny but high-leverage UX fix — typing a complex password into a
  masked field with no validation feedback is the #1 reason a real
  persona bounces from signup before they finish. Lucide icons keep
  it visually consistent with the rest of the auth chrome.

Cycle metrics (Day 20 snapshot):
- Pulled live numbers via the Supabase Management API:
  - 10 users total (2 real, 8 test/operator)
  - 0 subscriptions (no paying customers yet)
  - 8 clients (5 sample-seeded by signup auto-seed + persona spec)
  - 10 conversations / 22 chat messages
  - 16 approval items / 10 agent tasks
  - 9 audit logs (last entry 2026-04-27 — daily_briefing cron has
    been silent for 2 days; flagged for follow-up)
  - 4 founding slots claimed (all from persona spec CTA clicks; the
    claim-on-click-no-rollback-on-abandon pattern leaks counter
    space from test runs, noted for cleanup after first real cohort)
- signal_v2_current now records real numbers instead of TBD.

Verified: npm run type-check passes. Founding counter HTML on
production /pricing renders title='4 of 50 founding slots claimed'
(live path active). All 6 critical Stripe webhook event types are
handled with idempotent upserts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…16s to <300ms

Round 3 production-tier hardening pass — three findings, two shipped here.

Security headers (next.config.ts headers()):
The 4/29 audit found prod responses on /, /login, and /api/users/me
only carrying Strict-Transport-Security. For a paid SaaS that holds
client financial context that's nowhere near production-tier. Adds:

  - HSTS unified across all routes (max-age=2y, includeSubDomains, preload)
  - X-Frame-Options: DENY (clickjacking; Stripe Checkout uses full nav redirect)
  - X-Content-Type-Options: nosniff
  - Referrer-Policy: strict-origin-when-cross-origin
  - Permissions-Policy: camera/microphone/geolocation/payment all off
  - X-DNS-Prefetch-Control: on; X-Permitted-Cross-Domain-Policies: none
  - Content-Security-Policy-Report-Only with explicit allowlists for
    Stripe (js.stripe.com, api.stripe.com, hooks.stripe.com,
    checkout.stripe.com), Supabase (*.supabase.co), Resend
    (api.resend.com), Vercel Analytics + Insights, PostHog, and Sentry.
    Report-only first so we catch any legitimate inline source we
    missed before flipping to enforce.

Headers go through next.config's headers() instead of middleware so
they land on every response — static pages, redirects, and middleware
short-circuits — without the per-exit-point fragility of editing
middleware.ts (which has 8+ early returns).

/pricing performance:
The page had `dynamic = "force-dynamic"` + `revalidate = 0` which
turned every anonymous visit into a Postgres round-trip for the
Founding counter. Production TTFB measured 2.16s vs 150ms on /
(~14x slower) and would compound during a launch traffic spike.

Switched to ISR with revalidate=60. Display can be up to a minute
stale; the actual "is founding cohort available" allocation is still
atomic in /api/stripe/checkout (the SQL has WHERE claimed_count < cap
in the UPDATE so two concurrent attempts can't both succeed). No
oversell risk; just a much faster page.

Outstanding (next round):
- Vercel team is on Hobby plan → cron limited to 2 daily, but
  vercel.json registers 14. Audit_logs gap since 4/27 traces directly
  to this. Operator decision: Pro upgrade ($20/mo) or move agent
  crons to GitHub Actions / external cron.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vercel team upgraded to Pro by operator (4/29) — cron quota cap is
lifted, so this batch wires the work that was blocked behind it.

Founding-slot leak (FoundingClaim ledger + reconciliation cron):
- New schema model FoundingClaim — per-checkout-session ledger with
  status pending/confirmed/released. Indexed on (status, claimedAt)
  for the cron scan.
- src/lib/stripe/founding-slot.ts — claimSlot / abortClaim / confirmClaim
  / releaseStaleClaims helpers. claimSlot keeps the existing atomic
  Postgres increment (Postgres returns the post-update count, so two
  concurrent claims can't both pass the cap check) but ALSO upserts
  a FoundingClaim row keyed on the Stripe session id. confirmClaim
  promotes pending→confirmed when the webhook lands. releaseStaleClaims
  walks pending claims older than 25h, hits Stripe's session API, and
  decrements the counter for any session Stripe says is expired or
  stayed open past the 24h window. Reconciles webhook-loss too: if
  Stripe says the session is complete + paid but the claim is still
  pending, promote it as a fallback.
- /api/stripe/checkout uses claimSlot AFTER the Stripe Checkout
  session is created, so the ledger row carries the real session id.
  On race-loss (cohort filled mid-flight) the route returns 409 with
  a clear error rather than silently downgrading without telling
  the user.
- /api/stripe/webhook calls confirmClaim on every checkout.session
  .completed event whose metadata.is_founding === "true". Idempotent
  on retried deliveries.
- /api/cron/founding-slot-cleanup at 02:30 UTC daily. Slack pings
  when scanned > 0 with the (releasedExpired, reconfirmed, stillOpen,
  errors) summary — quiet on the trivial "0 stale" path.
- vercel.json adds the cleanup cron entry. Hobby was capped at 2
  (which silently dropped most of our 14 crons); Pro lifts that cap,
  so all 15 should fire on schedule starting tomorrow.

Production reconciliation done at commit time:
- Reset the 4 leaked claimed_count to 0 via Supabase Management API
  (all 4 were E2E persona-journey CTA clicks from this morning).
- The new ledger means every future founding cohort claim has an
  audit trail and self-reconciles within ~24h of the user abandoning.

CSP report sink (preparing for Enforce):
- /api/csp-report — POST sink for browser violation reports. Logs
  the full payload to Vercel logs (queryable via vercel logs --query).
  De-duplicates novel (directive, blocked-uri, document-path) tuples
  for Slack pings so a runaway page doesn't spam the channel.
  Rate-limited at 60 req/min per IP.
- next.config.ts CSP-Report-Only header now includes
  `report-uri /api/csp-report` so browsers know where to POST.
- New `csp_violation` notification type wired into the existing
  Slack adapter via the formatError fallthrough.

Verified:
- npm run type-check passes
- Vercel team plan confirmed = "pro" via API
- Manual cron fire (curl with x-vercel-cron header) returns 200 with
  ok:true, eligibleUsers:0 (all current users are e2e-personas without
  active client briefing windows — expected)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…channel was being spammed)

The /api/csp-report endpoint shipped in ca59fe4 was firing Slack
notifications on novel (directive, blocked-uri, document-path)
tuples with an in-memory dedup cache. In production this immediately
flooded #us-market-validation with empty "csp_violation" alerts:

  1. The dedup is per-Vercel-instance. Serverless cold starts reset
     the cache, so the same violation re-fired Slack on every cold
     boot. Behaved as ~no dedup at all under any meaningful traffic.
  2. CSP-Report-Only makes browsers send many reports per page load,
     not one. With 50+ unique violations across the marketing surface,
     the dedup set continually trimmed and re-evicted itself.
  3. The Slack adapter routed `csp_violation` through formatError with
     a `where:"csp_violation"` prefix but did NOT serialize the
     payload, so the Slack messages all read "위치 csp_violation /
     메시지 —" — empty rows that gave the operator zero signal.

Fix: keep the endpoint running so the report-uri directive still has
somewhere to POST (otherwise browsers see 404 and may surface their
own console errors), but make it log-only. To investigate violations:

  npx vercel logs https://practiq.dev --since 1h --query "csp-report" --expand

If we need a real dashboard later, build /admin/csp-report against
the log stream or a small DB-backed sink with a daily roll-up rather
than per-event Slack pings.

The rate limit (60 req/min per IP) and 400/429 handling are kept so
an adversary still can't abuse the endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…daries

A11y on /login + /signup:
- All form labels now have htmlFor pointing at input ids (login-email,
  login-password, signup-name, signup-email, signup-vertical,
  signup-password). Screen readers can now read each field with the
  correct name; click-target on label hits the input.
- Error blocks gain role="alert" + aria-live="assertive" so the
  error message is announced when "Invalid email or password" or
  "Account already exists" fires post-submit.
- Pre-existing aria-label on the password show/hide button (added
  in the Round 2 toggle work) is preserved.

Route boundaries:
- src/app/global-error.tsx — root error catcher. Renders OUTSIDE
  the root layout (per Next.js contract — must supply <html><body>),
  so it doesn't depend on Tailwind / fonts / providers that may have
  caused the failure. Uses inline styles only.
- src/app/error.tsx — marketing-side error boundary. Dark Practiq
  chrome, "try again" button + link back to home. Renders the trace
  digest if Next.js attaches one.
- src/app/app/error.tsx — authenticated dashboard error boundary.
  Same shape but keeps user inside their session ("Back to /app"
  not "Back to home").
- src/app/loading.tsx + src/app/app/loading.tsx — minimal "P" mark
  spinners on dark background so client navigation never flashes
  white. role="status" + aria-label so screen readers announce the
  loading state.
- src/app/not-found.tsx — branded 404 page with 5 high-traffic
  destinations (Homepage, Pricing, Use cases, Blog, Demo). Sets
  robots noindex/nofollow via metadata so Google doesn't index broken
  inbound links as crawlable URLs.

Verified: npm run type-check passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ction

Four new spec files lock in regression coverage for Round 4's work:

  tests/e2e/stripe-webhook.spec.ts (4)
    s01–s02  signature rejection paths (400 on missing / bad sig)
    s03      properly-signed non-founding event (200 + ledger untouched)
    s04      founding event flips FoundingClaim pending → confirmed.
             Spec forges signatures via stripe.webhooks
             .generateTestHeaderString with STRIPE_WEBHOOK_SECRET so no
             Stripe CLI dependency in CI / dev shells.

  tests/e2e/founding-slot-cleanup.spec.ts (2 pass + 2 skip)
    c01–c02  cron auth gates (401 anon / 401 wrong bearer)
    c03–c04  positive-auth + stale-claim error path. Skipped when
             CRON_SECRET isn't mirrored into local .env.local — the
             secret is production-encrypted only and operators
             generally don't pull it down. The skipped tests still
             document the contract for the day someone needs them.

  tests/e2e/csp-report.spec.ts (5)
    r01–r03  400 boundary cases (empty / invalid JSON / missing
             csp-report key) — locks in the 4/29 emergency log-only
             behavior so a future "let's add Slack again" PR fails the
             suite.
    r04      well-formed report returns minimal { ok: true } (no echo,
             no Slack signal, no PII surface).
    r05      production CSP header still points report-uri at
             /api/csp-report (so browsers don't 404 on report POST).

  tests/e2e/mobile-viewport.spec.ts (9)
    3 viewports × 3 pages: 360 / 768 / 1024 × / + /pricing + /login.
    Asserts h1 visibility + no horizontal scrollbar (≤ 18px slack for
    rounding + scrollbar gutter) + nav reachable (hamburger trigger
    OR desktop pricing link, depending on breakpoint).

Full suite result on https://practiq.dev: 49 passed / 2 skipped / 0
failed in ~1.3 minutes.

cycle events 98–99 record the spec ship + Round 5 verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t_logs gap since 4/27

The agent cron handler (nightly-briefing, anomaly-detector,
comms-drafter) was filtering users by `subscription.status in {active,
trialing}`. Since 0 paying customers exist + trial users don't get a
Subscription row until they convert, every cron run returned
`eligibleUsers: 0` and audit_logs has been empty since 4/27.

That breaks the AI-Native value demo: a trial user signs up, sees
the sample workspace once, never gets the "AI worked while you slept"
moment, and bounces. The whole product thesis evaporates at trial-
window time.

Fix: expand the WHERE clause to include trial users with no
subscription row whose createdAt is within the last 14 days.

  Eligible now =
    (Subscription.status in {active, trialing})
    OR (Subscription = null AND User.createdAt > now - 14 days)

Cost ceiling stays bounded:
  - dispatch.attempted === 0 short-circuits users without active
    clients (no API spend)
  - per-firm spend ceiling check inside dispatch
  - 14-day trial expiry naturally caps un-converted users

Tomorrow's nightly-briefing run (17:00 UTC = 02:00 KST) is the first
real test — should populate audit_logs for the 12 e2e personas + 3
operator users that have sample-seeded Acme Coffee Co clients.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s, contrast audit + DESIGN.md addendum

/pricing ISR tune (revalidate 60 → 300):
- Round 3 cut TTFB from 2.16s to 134ms warm by going from
  force-dynamic to revalidate=60. Round 6 measured the cold-cache
  spike: 880ms first hit after expiry. With revalidate=60 every
  minute's first visitor pays the full regen cost — about 1.7% of
  requests hit cold.
- Bumping to revalidate=300 (5 min) drops cold hits to ~0.3% of
  requests (5x improvement) at the cost of cohort-counter staleness
  up to 5 min. Allocation safety stays atomic in /api/stripe/checkout
  + the FoundingClaim webhook reconciliation, so a stale display
  number can't oversell the cohort.

founding-slot-cleanup maxDuration 60 → 120s:
- Each pending claim costs one stripe.checkout.sessions.retrieve
  (~200-400ms). With the 200-row scan cap, worst-case loop is
  20-80s. Round 4 capped at 60s out of caution; Round 6 raises to
  120s now that we're on Pro and per-row try/catch handles slow
  tails individually.

Color contrast audit + DESIGN.md addendum:
- Computed WCAG ratios on every dark-theme text token. zinc-100
  (18.5:1) and zinc-200 (16.1:1) pass AAA. zinc-400 (7.95:1) passes
  AAA. zinc-500 (4.22:1) only passes AA-large. zinc-600 (2.64:1)
  FAILS even AA-large for body text — must be reserved for icons /
  dividers / decoration only.
- DESIGN.md text-token table now shows the ratio + WCAG result
  next to each token so future PRs pick correctly.
- Fixed the worst hot-path offender: footer copyright row was
  text-zinc-600 → text-zinc-500 (still small but at least at the
  AA-large threshold for the 10px legal print).

Verified: npm run type-check passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds tests/e2e/auth-flows.spec.ts (10 tests) covering the auth
recovery surfaces persona-journey doesn't exercise:

  a01–a02  /forgot-password page + submission (account-enumeration
            defense — unknown email STILL returns "Check your inbox"
            so an attacker can't probe which addresses exist).
  a03–a04  /reset-password/<token> + /verify-email/<token> render
            gracefully with bogus tokens — no 500, no error-boundary
            leak (proves the new app/error.tsx is reachable but NOT
            triggered on this code path).
  a05–a07  /api/auth/forgot-password (200 valid / 400 missing) +
            /api/auth/resend-verification (401 anon, auth-gated by
            design — guards against arbitrary-recipient mail abuse).
  a08      "Forgot?" link from /login → /forgot-password navigation.
  a09      POST /api/chat 401 anon — LLM cost-protection regression.
  a10      PATCH /api/users/me 401 anon — schema-bug detector
            regression (the route that led us into the preferred_model
            P2022 hunt earlier today).

Full E2E suite now at 91 passed / 19 skipped / 0 failed across 14
spec files (2.2 minutes). The 19 skipped are DOGFOOD-credentials-
required tests (auth-chat-flow.spec.ts) + CRON_SECRET-required
tests (founding-slot-cleanup.spec.ts c03/c04).

Welcome email content audited via Resend dashboard: vertical-aware
copy, Practiq logo, light-mode email chrome that contrasts with
the dark in-app design (correct — emails always go light). Quality
is acceptable.

cycle events 105–106 record the spec ship + Round 7 close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds GET /api/health for external uptime monitors (UptimeRobot,
BetterStack, Pingdom). Anonymous + cacheable for 30s by Vercel
edge so a 1/min monitor hits the cache more often than not;
?fresh=1 bypasses for one-off operator checks.

Checks (each capped at 1.5s):
  - db (REQUIRED)        SELECT 1 from Postgres via Prisma
  - stripe (REQUIRED)    stripe.balance.retrieve() — auth-only, ~150ms
  - resend (soft)        GET /domains 2xx — domain status
  - anthropic (soft)     env-key presence only (a real probe would
                         cost ~$0.0005 per check, $0.72/day at 1/min)

Returns 503 when either required check fails so the monitor flips
to "down" before users see 5xx on real routes. Returns 200 when
all required checks pass.

tests/e2e/health.spec.ts (5 tests):
  h01  200 + ok:true on healthy prod
  h02  response shape — all 4 checks present with {ok,ms,detail?}
  h03  required-checks gate overall ok flag
  h04  cache-control header — default cacheable, ?fresh=1 no-store
  h05  anonymous (no auth/secret needed)

Why a health endpoint at all: production-tier expectation. Currently
practiq.dev has no equivalent, so a partial outage (DB up, Stripe API
down) only surfaces when a user tries to checkout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-Round-9 the "Run your first briefing" onboarding step had
href="/app" — clicking it just stayed on the same page with no
visible feedback. The HomeAgentCTA button that actually fires the
agent run was scrolled below the fold for new users.

Fix:
- Adds id="run-briefing" + scroll-mt-16 to the agent-CTA section so
  /app#run-briefing scrolls the user straight to the "Run now" button.
- Updates the onboarding step to that anchor so "Run now →" actually
  takes the user somewhere useful.
- Sharpens the description copy: "Scheduled briefings land overnight,
  but you can fire one now and watch the agent populate the Approval
  Queue in real time" — sells the AI-Native moment instead of just
  describing the mechanic.

Also strengthens the first two steps' copy:
  - Step 1: leads with the differentiator ("scoped AI memory — that's
    what makes Practiq different from chat-session AI") instead of the
    feature description.
  - Step 2: explains why pinning matters ("ride along on every future
    conversation") instead of just describing the action.

Trial users now have a 5-second path to the AI-Native moment instead
of waiting until the next nightly cron run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… + reset-password

Adds tests/e2e/token-flows.spec.ts (6 tests) that fabricate real
VerificationToken rows via the Supabase Management API, then submit
them to the corresponding POST endpoints, then verify the side
effects on the User row. Pre-Round-9 these write paths had ZERO E2E
coverage — bogus-token tests in auth-flows.spec.ts proved nothing
about the happy path actually committing the right state.

  t01–t02  verify-email 400 boundaries (no token / bogus token).
  t03      Valid pending verify-email token → User.emailVerified
            gets set; second consume returns 200 alreadyVerified=true
            (idempotent, no double-flip).
  t04–t05  reset-password 400 boundaries (missing fields / short pw).
  t06      Valid reset-password token → password_hash actually
            changes AND bcrypt.compare(newPw, hash) succeeds AND
            second use of the same token returns 400 (one-shot
            consumption — protects against replay).

Each happy-path test cleans up its provisioned user + token so
re-runs don't accumulate test rows in production.

Full E2E suite now at 102 passed / 19 skipped / 0 failed across
16 spec files (2.8 minutes). Spec coverage growth this session:
  Round 5  49 / 6 files
  Round 7  91 / 14 files
  Round 8  96 / 15 files
  Round 9  102 / 16 files

cycle events 109–110 record the Round 9 ship + close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dashboard's server component ran ~13 Prisma queries serially:
user, clients, contexts, pendingCount, topPending, contextCount,
agentTaskCount, reviewedCount, outgoingInviteCount, userPlan,
periodStart (sub or user), chatThisPeriod, plus sample-derived
subtractions. Cold TTFB measured 1.2s; warm 322ms.

Restructured into two waves:

  Wave 1 (10 independent userId-scoped reads, all in Promise.all):
    user.findUnique (now also pulls createdAt for the periodStart
      fallback so we don't re-read user later)
    client.findMany
    clientContext.findFirst (lastContextUpdate only — was findMany
      take 1, switched to findFirst with select)
    approvalItem.count (pending_review)
    approvalItem.findMany (top-4 with client include)
    clientContext.count
    agentTask.count
    approvalItem.count (reviewed)
    teamInvite.count
    resolveUserPlan

  Wave 2 (depends on Wave 1):
    subscription.findUnique OR user createdAt fallback for periodStart
    usageEvent.count for chatThisPeriod

  Wave 3 (only when sampleClient is present):
    sample contextCount + sample agentTaskCount in Promise.all (was
    serial)

Net: serial waterfall reduced from ~13 hops to 3 (Wave 2 short-circuits
when subscriptionId is null and reuses the already-fetched me.createdAt).
Expected cold-TTFB drop ~40-50% based on the per-query latency profile
seen in the /api/health probe (db 666ms for a SELECT 1 means each
serial round-trip was ~50-100ms in our config).

Also folds the periodStart user re-read into the initial me query,
saving one round-trip on the free-trial path (which is currently
100% of users).

No semantic changes — all WHERE clauses + counts identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 new tests in tests/e2e/misc-endpoints.spec.ts cover the orphan
endpoints that don't fit cleanly in the persona / auth / token specs:

  m01  /api/auth/available-providers anonymous JSON shape (the
        OAuthButtons component on /login + /signup reads this; the
        spec locks the contract so a future shape regression breaks
        the suite, not the live login page).
  m02  /api/agents/run 401 anon — LLM-cost protection. This is the
        per-user "run my own briefing now" endpoint that the
        HomeAgentCTA and onboarding step 3 fire.
  m03  /api/team/invites POST 401 anon — outbound invites are
        auth-gated; inbound consumption happens at /api/auth/signup.
  m04  /robots.txt + /sitemap.xml + /llms.txt + /llms-full.txt all
        200; sitemap carries >= 100 <loc> entries (currently 506).
  m05  Homepage SEO contract — html lang="en", rel=canonical, +
        og:title/description/image (locks the JSON-LD work from
        RUN 22 against accidental layout-component refactors).

cycle events 111-112:
  - seq 111  feature_shipped  Round 10 perf + misc-endpoints
    (commits 33734ee + this one, deploy nxvpjykiv)
  - seq 112  verified  Full suite at 107 pass / 19 skip / 0 fail
    across 17 spec files (2.6 min). Spec coverage growth this
    session: r5=49 → r7=91 → r8=96 → r9=102 → r10=107.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Fractional CFO copy)

The pre-Round-11 README was based on the original Fractional Executive
positioning and described:
  - target persona = Fractional CFO/COO/CTO (we pivoted to boutique
    professional services firms in April 2026)
  - dashboard route = /(dashboard) (the actual route is /app)
  - tech stack = Supabase Auth + Supabase Storage (we use NextAuth.js
    + local + Stripe; Supabase is Postgres-only)
  - /api/documents endpoint (deleted in Round 4 as dead code; the
    real generate_document tool lives in src/lib/claude/tool-handlers.ts)
  - Phase 0 problem-validation goals (we're in Phase 6 product-validation
    after the 4/23 pivot, formalized 4/27 via interrupt seq=72/73)

New README covers the actual production state:
  - Practiq positioning ("AI built around your clients, not your chats")
  - Three-product-paradigm differentiation table
  - Tech stack as deployed (NextAuth v5 + Stripe + Resend + Vercel Pro
    + Anthropic + Supabase Postgres)
  - Routes overview (marketing / auth / authenticated /app / admin /
    api / 15 cron schedules)
  - Local dev with the studio-root .env.local pattern
  - Testing — 107 passed / 19 skipped E2E + per-spec coverage table
  - Cycle state pointers
  - Documentation index

Live URLs (https://practiq.dev + admin.grindworks.ai) called out so a
new reader doesn't need to grep .cycle/state.json to find them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arding catch paths

Round 11 audit found 33 console.error sites across src/app/api +
src/lib but only 3 also fired safeNotify("error", ...). That meant
a real user hitting a 500 on /api/auth/signup (e.g. earlier today's
P2022 preferred_model column-missing incident) got a generic
"Internal server error" reply and a silent Vercel log line — no
Slack ping, no operator alert until someone proactively grepped.

This commit:

  - Adds src/lib/observability/notify-server-error.ts. Single helper:
    `notifyServerError(where, error, context?)`. Always logs to stdout
    (queryable via `vercel logs --query "where"`), always fires a
    fire-and-forget Slack alert (`safeNotify("error", { where,
    message, ...context })`). Never throws.

  - Wires the helper into the 4 most user-facing catch blocks:
      /api/auth/signup            (top-level try/catch)
      /api/onboarding/sample      (POST seed + DELETE remove)
      /api/chat                   (SSE stream failure)

  - Leaves the helper documented as a one-line drop-in replacement
    for `console.error("[where] err:", err)` so the rest of the
    audit's 30+ call sites can adopt it incrementally as they're
    touched, without a big refactor PR.

Why not auto-Slack on every console.error: we don't want every
"oh that prisma update failed but we caught it" warning to ping the
operator. The helper marks the strict subset of catches that mean
"the user got a 5xx" — which IS production-tier alerting territory.

cycle events 113-114 (114 retro-records the README rewrite).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
seungdo-keum and others added 22 commits May 18, 2026 15:49
…ement)

Root cause of the 2026-05-18 Vercel cron 500: serverless function fs
is READ-ONLY except /tmp. The send route attempts to move JSON files
draft → outreach-sent/ AND modify target-list-*.csv in place after a
successful Resend send. Both fail with EACCES under /var/task/.cycle.
The file-tracer fix (d192457) brings the files in but they're not
mutable at runtime.

Solution: move the daily send loop to GitHub Actions where the runner
has a writable cwd and can commit state changes back to main. Vercel
cron route stays as the read-only health endpoint but is no longer
the source of truth for send scheduling.

New files:
  - scripts/discovery-send.mjs — CLI replica of the cron route's send
    logic. Reads drafts, applies ramp/confidence/suppression checks,
    sends via Resend, writes state changes locally. Same reputation
    safeguards as the cron version. Suppression check via Supabase
    Management API (SUPABASE_ACCESS_TOKEN), fail-closed.
  - .github/workflows/discovery-send-daily.yml — Schedules send at
    14:00 UTC Mon-Fri. workflow_dispatch supports dryRun=true and
    limit override for manual testing. Commits moved drafts + CSV
    diffs back with [skip ci] tag.
  - package.json — adds `discovery:send` npm script.

GH Secrets synced from studio .env.local (verified 2026-05-18):
  - RESEND_API_KEY ✓
  - RESEND_FROM_EMAIL ✓
  - SUPABASE_ACCESS_TOKEN ✓
  - SLACK_WEBHOOK_URL ✓
  - OUTREACH_REPLY_TO (empty in local; optional)

Local dry-run verified end-to-end:
  > npm run discovery:send -- --dry-run
  Pending drafts: 11
  Daily cap: 5 (week 1 ramp)
  Dry-run pass: 5 (adrian@anlcpa.com, admin@whiteglovecpas.com,
                   info@adifferentcpa.com, jf@bifrost-partners.com,
                   michelle@gsa.cpa)
  Queued: 6 (process tomorrow)
  Confidence floor 0.6 applied. No suppressions hit.

First production trigger should pass workflow_dispatch with
dryRun=true to verify the runner can read drafts + suppress + commit
state. Once green, the scheduled cron starts producing real sends
Tuesday 14:00 UTC (next weekday from this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…GSC analyzer

Three changes:

1. Big discovery batch v2 — 112 verified US emails (2.8x prior baseline 40)
   - count=300 across 8 high-yield sources (yellowpages, osm-overpass,
     competitor-customers, taxbuzz-sitemap, sba-loans, state-cpa-boards,
     accounting-today, cpa-practice-advisor)
   - firm-name-resolver unlocked 70/150 (47%) SBA/state-board rows
     that previously sat unused
   - web-enrich concurrency 8 + politeness 150ms shaved wall-time
   - 38 non-US firms filtered by isLikelyUSLocation (Canada/UK/etc)
   - discovery:draft generated 10 NEW ready drafts (queue now 16
     total). 70 skipped on insufficient context — these were resolver-
     surfaced firms where the firm site lacked enough detail for
     Claude to anchor a personalized email; we keep firm_url for
     future use but don't waste sends on generic copy.

2. Bot UA filter strengthened — `moto g power (2022)` pattern added
   2026-05-18: detected fake-mobile bot fleet hitting /workflow-audit
   from 8+ US cloud DCs (AWS Boydton, Azure Cheyenne, Des Moines,
   etc) with identical Android UA. 96/99 workflow_audit_started
   events were from this fleet. Zero subsequent conversions of any
   kind (0 signup, 0 demo click, 0 checkout). Pure noise.
   - scripts/daily-pulse.mjs BOT_UA_PATTERNS gets the new pattern
   - src/app/api/cron/daily-pulse/route.ts (Vercel cron version) gets
     the same pattern so the morning Slack pulse stays consistent
   - Re-running pulse after the fix: 7d visitors 907→472 (-48% noise);
     workflow_audit_started 99→3; "0% completion" trigger no longer
     fires (the funnel was bot-inflated, not actually broken)
   - Operator+bot filtered DAU 71→44 raw

3. New scripts/gsc-top-queries.mjs — GSC top-30 queries by
   impressions, useful for spotting "ranked but no click" titles.
   Initial pull revealed 8+ queries where Practiq sits on page 1
   (positions 1-7) with 0 clicks despite 30-170 impressions each.
   Title/meta rewrite is the immediate ROI lever — same impressions
   pool, +1-3% CTR is +5-20 clicks/week. Examples:
     gusto and bamboohr consultant            171 impr pos 5.4
     karbon vs taxdome                        111 impr pos 30.5
     karbon vs canopy                         107 impr pos 31.7
     peo vs hr consultant vs software ...      39 impr pos 1.2
     fractional hr services with bamboohr ...  31 impr pos 5.5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two protective changes to the cold-outreach pipeline:

1. Role-based mailbox filter (info@, admin@, ask@, hello@, etc.)
   These mailboxes route to whoever's on info-monitoring duty rather
   than a real decision-maker. Cold-outreach reply rate is 5-10x worse
   and they often trigger distribution-list auto-replies that damage
   sender reputation.

   Of the 5 emails sent in the 2026-05-18 batch via Resend, 4 were
   role-based (admin@lucrumaccounting, admin@whiteglovecpas,
   info@adifferentcpa, help@ignitionapp — the last is a vendor inbox,
   not a target firm anyway). Only adrian@anlcpa.com was a real
   person-named lead. That's poor signal-to-noise.

   - scripts/discovery-draft.mjs: filter candidates by isPersonNamedEmail()
     BEFORE Claude personalization. Role-based rows stay in the CSV
     for manual operator action via signals.csv; they just don't enter
     the auto-send pipeline.
   - scripts/discovery-send.mjs: defense-in-depth guard at send time
     using the same ROLE_LOCALS set. Skipped drafts get moved to
     outreach-sent/ marked as skipped=role_based_mailbox so they
     don't sit in the queue forever.
   - 10 existing role-based drafts moved from outreach-drafts/ to
     outreach-role-review/ so tomorrow's 14:00 UTC cron doesn't send
     them. Operator can manually triage from that folder.

2. Git-history rescue: scripts/rebuild-target-list.mjs
   Each discovery:source run with the same date+vertical overwrites
   target-list-{date}-{vertical}.csv. Batches 1 (157 emails) and 2
   (112 emails) were collapsed to whatever the most recent run had.
   This rescue script walks `git log` for the CSV, unions all
   historical row sets + current working tree + emails referenced in
   outreach-drafts/ / outreach-sent/ / outreach-role-review/, dedupes
   by lowercased email, and writes one combined CSV.

   Re-run after the rescue: 113 unique emails recovered (46 person-
   named cold-eligible + 67 role-based held for manual review).

   discovery:draft on the rescued set produced 0 NEW drafts on the
   30 fresh person-named rows — every one was held by Claude's
   INSUFFICIENT_CONTEXT refuse (firm name + city alone isn't enough
   to anchor genuine personalization). This is by design — quality
   over quantity. To unlock more drafts, the rows need additional
   context (firm bio, LinkedIn snippet, public quote, recent firm
   news). Separate enrichment iteration.

Net effect on tomorrow's 14:00 UTC scheduled cron:
  - Queue has 6 person-named drafts (down from 16 — 10 role-based
    held back, 0 new from refill).
  - Cron sends min(6, cap=5) = 5 person-named to: david@davidbagleycpa,
    jf@bifrost-partners, john.bovard@bovardcpa, michelle@gsa.cpa,
    tom@dotzenrodinc.
  - yrhl@yarling sits at the bottom of the queue for Wednesday.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five blog-post titles + meta descriptions rewritten to match the
exact searcher phrasing for the eight GSC queries that have been
sitting on page 1 (positions 1-7) but earning 0 clicks across 464
impressions/28d. The previous titles either led with the wrong
keyword (Rippling first when the query was Gusto+BambooHR; Clio vs
MyCase first when the query was Clio vs PracticePanther vs MyCase)
or buried the search-intent term inside a generic framing
("Looking Beyond QuickBooks" instead of "Best QuickBooks
Alternative"). All five new titles are <=66 chars and lead with
the query verbatim; descriptions are 155-163 chars and preview the
concrete answer (cost spread, decision criteria, comparison
dimensions) rather than restating the question.

Per-query coverage:
- gusto and bamboohr consultant -> rippling-vs-gusto-vs-bamboohr-for-hr-consultants
- clio vs practicepanther vs mycase trust accounting -> clio-vs-mycase-vs-practicepanther-solo-small-firms
- clio vs practicepanther vs mycase for immigration -> same
- clio manage vs mycase vs practicepanther -> same
- clio vs practicepanther vs mycase client intake -> same
- peo vs hr consultant vs software for small business -> peo-vs-hr-software-vs-hr-consultant
- as a accountant best quickbooks alternative -> quickbooks-alternative-accountants
- fractional hr services with bamboohr or gusto -> fractional-hr-services-explained

Also adds two helper scripts:
- scripts/gsc-query-pages.mjs (resolves GSC query -> ranking URL via SA)
- scripts/check-meta-lengths.mjs (asserts title/desc char budgets)

No body content changed — title/description rewrite only. GSC will
pick up new titles in 2-7 days; CTR improvement visible 14-28 days
later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… pages

Adds practiqProductJsonLd() helper + PRACTIQ_CANONICAL_DEFINITION
constant, wires both into /alternatives/[tool], /best/[slug],
/for/[vertical], /vs/[slug], and the homepage. The Product schema
cross-references the global SoftwareApplication via @id so AI engines
(Perplexity, ChatGPT, AI Overview) see Practiq as a named answer-set
entity on listicle and comparison pages rather than just one of many
inline links. The canonical-definition paragraph lives in raw
server-rendered HTML (data-aeo="canonical-definition") so the
"Practiq is X for Y" sentence is extractable even when the page
bails out to CSR.

Also adds scripts/aeo/citation-scan-local.ts — a no-DB-write copy of
the production GEO scan that operators can run against any deployed
branch to compare citation rate before/after schema changes. The
2026-05-18 baseline (3/12 = 25%, openrouter only) is committed at
.cycle/research/aeo-scan-2026-05-18.json for diffability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GSC shows Practiq ranking page 3-4 (positions 28-50) for ~520
impressions/mo of exact-match competitor-vs-competitor queries
("karbon vs taxdome", "karbon vs canopy", "canopy vs taxdome",
"jetpack workflow vs karbon", "karbon alternatives"). The existing
/vs/[slug] dynamic route renders templated short pages from VS_PAIRS
data which underperforms for these high-intent commercial queries.

Ships five dedicated route files under src/app/vs/{slug}/ — each
2,500-2,750 words of operator-grade content with sticky TL;DR
comparison table, six expanded sections (workflow, portal, AI,
pricing, mobile, integrations, support), operator picks by firm
shape, real review quotes from G2/Capterra/Reddit with source URLs,
8-item FAQs, and the "where Practiq fits" angle at the end (not the
top — search intent is comparison, not pitch).

Shared layout at src/components/vs/long-form-vs-layout.tsx so the
five pages reuse the same dark theme, glass panels, sticky index nav,
and CTA shape. Each page emits its own Article + FAQPage +
SoftwareApplication + BreadcrumbList JSON-LD tuned to that pair.

Surfaces:
- /vs/karbon-vs-taxdome (111 impressions/28d, pos 30.5)
- /vs/karbon-vs-canopy (107 impressions/28d, pos 31.7)
- /vs/canopy-vs-taxdome (60 impressions/28d, pos 45.0)
- /vs/jetpack-workflow-vs-karbon (54 + 36 impressions/28d, pos 28-35)
- /vs/karbon-alternatives (34 + 35 impressions/28d, pos 37-48)

Sitemap entries at priority 0.85 (higher than VS_PAIRS template
variants), /vs index page promotes them in a top-of-page section
above the existing Practiq-vs-X cluster. Static routes naturally
override /vs/[slug] for the colliding karbon-vs-canopy slug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fts)

Background run beozkicq7 with uncapped resolver + persistent cache +
concurrent workers (commit 7ca0a6e) processed:
  Sources run:        10 high-yield (yellowpages, osm-overpass,
                      competitor-customers, taxbuzz-sitemap, sba-loans,
                      state-cpa-boards, socrata, cpa-practice-advisor,
                      accounting-today, bbb)
  Raw candidates:     8,443
  Total email-bearing: 412 (vs 113 baseline = 3.6×)
  Total signals:      7,799
  Firm-URL resolved:  406/7,030 via DDG search (5.8% hit rate — lower
                      than the prior 47% measurement on 150 samples;
                      lower because SBA/state-board firm names are
                      heavily abbreviated and DDG returns noise)
  Web-enrichment:     292 signals → emails (Path 1 scrape of firm
                      contact/about/team pages)
  US-location-filter: 101 non-US firms dropped

Combined inventory rebuild (scripts/rebuild-target-list.mjs unions all
historical CSVs + drafts + sent + role-review):
  Total unique emails: 413 (vs 113 prior = 3.7×)
  Person-named:        230 (vs 46 prior = 5.0×)
  Role-based:          183 (held in outreach-role-review/)

discovery:draft on 230 person-named: 5 NEW ready drafts + 208 held by
Claude's INSUFFICIENT_CONTEXT refuse. Net queue: 11 person-named
drafts ready for the next 14:00 UTC cron (week 1 cap=5, so ~2 days
of sending).

Why only 5/230 drafts? Email is captured but the personalization
context isn't. YellowPages / OSM / SBA / state-boards give us firm
name + city + sometimes website. Claude refuses to fabricate
generic copy. The 5 that passed all had concrete anchors from SBA
jobs_supported, Socrata business-since-year, or firm bio in
competitor-customers case study.

This is the bottleneck the next wave of source-acquisition agents
(X/Y/Z/W) explicitly targets:
  - Agent X (associations): NAEA/NATP/AICPA-PCPS member directories
  - Agent Y (events): conference speaker bios = 100-300 word
    self-descriptions, perfect personalization fuel
  - Agent Z (rankings): "Congrats on the #87 IPA ranking" hook
    auto-provided
  - Agent W (LinkedIn/podcasts): public headlines + verbatim bios

Sample new drafts (real firm context anchors):
  - Austin Niester Schweihofer & Finnegan — "Supporting 12 jobs in
    Port Huron since 2010" (SBA jobs_supported anchor)
  - J.E. Wiggins & Co. — "38 years in Columbus" (Socrata years-in-
    business)
  - Bifrost — "Question about scaling past 20" (competitor case study)
  - Tina Accounting & Tax — "Running a 3-person tax firm in Frankford"

.gitignore: added .cycle/_cache/ — firm-URL resolver cache grows
into multi-MB JSON, no value in committing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
271 new discovery sources across:
- Healthcare specialty (AAFP, ASHA, AAO, ADA, AVMA, ACA, NAMI, AAMFT, ACVS)
- Coaching/speakers/talent/exec-search (BigSpeak, All American, AESC, ERE, NAPS)
- Event/wedding planners (WeddingWire, TheKnot, Eventbrite, Cvent)
- Tech specialty (Zapier, Zoho, IBM, Oracle, ServiceNow, RedHat partners)
- Education/ESG (NEPC, AEA, GuideStar, Candid, Schoolhouse)
- Government affairs (NAGW, AAPA, NACAC, NCEA, NAEM)
- CSS certs / WHOIS / DNS-MX (sector gTLD enumeration)
- Reddit profile bios + firm mentions
- YouTube firm channels + CPA creator channels
- Wave-5 healthcare (NPI, Medicare, CMS providers)
- Wave-13 boutique professional services

Plus:
- scripts/merge-all-sources.mjs — union+dedup all artifacts preserving raw
- scripts/rebuild-target-list.mjs — git-history email recovery
- scripts/discovery-sources/firm-name-resolver.mjs — persistent 30-day cache
- Orchestrator now 274 sources, 277 imports
- _shared.mjs: 62+ verticals (coaching, speaker_bureau, talent_agency,
  executive_search, healthcare specialty, ESG, government affairs)

Cumulative inventory baseline 198 unique / 63 emails / 28 cold-mail-eligible
preserved as master-inventory.csv. Wave-5+13 collection in progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/enrich-signals-now.mjs reads master-signals.csv, filters US
rows with firm_url, calls enrichBatch in parallel, appends extracted
emails to today's target-list CSV.

Designed for "I have N signals sitting idle — convert as many to
emails as possible without re-scraping". The 2519 signals from the
direct-API sweep (NPI/SBA/USAspending/Form5500/IRS-PTIN) all have
firm_url populated. Args: --max=N (default 800), --concurrency=N (8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tage 5)

Two bugs in merge-all-sources.mjs corrected:

1. isPersonNamed too lenient — only checked email-local against ROLE_PREFIXES,
   never required contact_name to have a person-shape (FirstName LastName).
   This is why "Cold-mail-eligible: 1,920" in the v1 metric while the actual
   personalize-able inventory was 27. Fix: require contact_name with >=2
   alpha tokens of pattern [A-Za-z][A-Za-z'.\-]+.

2. git-history union was hard-coded to two files (target-list-2026-05-18-
   accounting.csv, signals-2026-05-18-accounting.csv). When the operator ran
   a mega-sweep on a new vertical or date the prior data silently dropped
   from master inventory. Fix: discover historical files dynamically via
   `git log --all --name-only -- KIT/target-list-*.csv KIT/signals-*.csv`.

Result: cold-mail-eligible metric drops from misleading 1,920 to the honest
27, matching the Stage 1 audit's careful analysis. All signal-only data
from prior mega-sweeps now survives across new vertical sweeps via git
history union.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ield

JobSpy (Week 1 action from researcher manual):
- scripts/jobspy-hiring-trigger.py (free, no API keys, MIT)
- 70 calls × Indeed/LinkedIn/Glassdoor = 642 unique boutique CPA firms
- Hiring-trigger metadata for personalization anchor

Healthcare mega-sweep (bj1g35q9a, 5h 18m): 1,978 emails + 5,064 signals

Master inventory: 8,212 → 14,043 (+71%). Lenient 4-tier USABLE: 1,888 → 1,924.
13 more verticals still running (~2-4h ETA).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 3 mega-sweep stalls (memory pressure, then Jina IP-shared
rate-limit accumulation), 29 partial-2026-05-19-*.jsonl files (1.36GB)
were preserved via the incremental-save patch. This commit lands the
recovery pipeline:

  scripts/partial-jsonl-to-csv.mjs  (new) — converts each partial
    JSONL to target-list / signals CSV, dedupes by (email || firm+loc),
    skips clobbering already-completed vertical CSVs

  scripts/bg-monitor.sh / -v2 / -v3   (new) — selective Monitor
    feeds (HEARTBEAT + VERTICAL_DONE + MEMORY_LOW/HIGH + QUALITY_LOW).
    v3 dropped STALLED noise; v2 had per-vertical hot/warm/cold gauge.

  scripts/jobspy-hiring-trigger.py  (new) — free LinkedIn/Indeed/
    Glassdoor/ZipRecruiter scrape; 7 queries x 10 cities → 642 CPA-
    hiring firms emitted as signals-{date}-jobspy.csv

Recovery yield (24 verticals processed; 5 already had CSVs):
  partial JSONL rows merged:   825,536 input → 390,550 unique
  master-inventory (emails):   3,897 → 4,111  (+214)
  master-signals (no email):   132,631 → 386,439  (+253,808)
  USABLE 4-tier (A+B+C+D):     2,656 → 2,758  (+102)

Notable per-vertical recoveries:
  accounting: 366 emails / 122,692 signals
  consulting: 103 emails / 40,119 signals
  mental_health: 0 emails / 108,429 signals
  dev_shop: 32 emails / 15,462 signals

Open quality issue (logged in monitor as QUALITY_LOW):
  Many recovered verticals show signal:email ratios > 200:1,
  meaning firm_urls were collected but contact extraction failed —
  likely dead/parked sites or web-enrich rate-limited. Worth a
  follow-up sweep with a firm-url-validity pre-check before
  spending the web-enrich budget.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…als)

partial-jsonl-to-csv.mjs previously crashed with "Invalid string length"
on partials >512MB (V8 string limit) — 3 critical files affected:
  healthcare.jsonl     1.2GB
  mental_health.jsonl  688MB
  accounting.jsonl     561MB

Fix: replace fs.readFile with readline streaming via fssync.createReadStream
+ readline.createInterface. Handles arbitrary file sizes line-by-line, no
heap allocation for full file content.

Verified working:
  mental_health   streamed 688MB / 910,511 lines
  talent_agency   streamed 113MB / 138,379 lines
  veterinary      streamed 115MB / 140,621 lines

Re-running consolidation + merge yielded:
  master-inventory:  32,934 → 32,938 emails  (+4, saturated)
  master-signals:   552,915 → 744,051 signals (+191,136 ← streaming unlocked these)
  total unique:     585,849 → 776,989 rows  (+191K)

Master merge now needs --max-old-space-size=8192 (8GB heap) due to
the 744K-row in-memory dedup. Future optimization: streaming merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5 polish for npm publish readiness. Verified via `npm pack --dry-run`:
48 KB packed (down from 53 KB), 79 files (LICENSE added, dist/ only, src/ excluded
by implicit .gitignore rule + explicit `files` field).

Changes to package.json:
- name: @grindworks/practiq-mcp → @practiq/mcp (scope verified available 2026-05-21
  via npm view → 404 = unregistered, claimable)
- license: MIT → AGPL-3.0-only (matches CHK-2 decision; whole-repo single license)
- add files: ["dist", "README.md", "LICENSE"] for tarball precision
- add prepublishOnly: type-check + build (blocks accidental stale dist publish)
- engines.node: >=18 → >=20 (Claude Desktop runs Node 20+; cuts support tickets)
- homepage + bugs + expanded keywords for npm discoverability
- repository.url + directory point at upcoming new public repo
  (github.com/practiq/practiq.git, directory: packages/mcp) — to be created in Phase 7

New file:
- LICENSE: AGPL-3.0 canonical text (661 lines, GNU.org source)

References:
- .gtm/01-mcp-audit.md (Phase 1 audit; punch list items 1, 3, 4, 7)
- .gtm/recovery-cso-audit.md D4 (npm publish supply-chain checklist)
- .cycle/decisions.md (CHK-2 AGPL-3.0 approved 2026-05-21)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 security-audit follow-up. gitleaks flagged the IndexNow KEY constant
in indexnow.ts as generic-api-key, but IndexNow keys are public by protocol
design (search engines fetch https://<host>/<key>.txt to verify ownership).

Still moves the literal to an env var because:
  1. Silences gitleaks on future scans (.gitleaksignore not needed)
  2. Lets self-hosters bring their own key + host without forking
  3. Standard 12-factor practice

Backward-compatible default `practiq76081581` + `practiq.dev` preserved when
env vars are unset, so production behavior is unchanged.

References:
- .gtm/02-security-audit.md (Phase 2 audit, "Inside OSS scope" section)
- .gtm/recovery-cso-audit.md (D1, mitigation row)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…want/practiq-oss

Operator confirmed 2026-05-26: the new public OSS repo lives under the
existing cliwant GitHub org and the existing @cliwant npm scope. Matches
Cliwant's switchsignal-oss / switchsignal-mcp naming convention.

Verified availability:
- gh api orgs/cliwant: 11 public + 97 private repos, gh CLI accessible
  with current PAT (scopes: repo, delete_repo, workflow)
- npm view @cliwant/practiq-mcp: 404 (claimable)
- npm view @cliwant/practiq: 404 (claimable, alternate)

Files touched (37 .gtm/ files + 1 mcp-server/package.json = 162 string
replacements via .gtm/_scope-rename.py one-shot script):

- @practiq/mcp           -> @cliwant/practiq-mcp
- github.com/practiq/*   -> github.com/cliwant/practiq-oss
- <org>/practiq          -> cliwant/practiq-oss
- PRACTIQ_REPO default   -> cliwant/practiq-oss

Production code update:
- mcp-server/package.json: name + repository.url + bugs.url all updated
- type-check + npm pack --dry-run both still PASS (per .gtm/recovery-cso-audit.md D4)

Phase 7 runbook updated:
- ORG="cliwant" REPO="practiq-oss" baked in
- "What is locked in" section replaces the prior "what to decide" prompts
- npm scope ownership step clarified (scope exists; operator only needs login)

The rename script is committed for audit / future re-runs.
SKIP_FILES list explicitly excludes raw scan outputs and the .gtm/01-mikeoss-research.md
(which references willchen96/mike, not Practiq).

References:
- .gtm/_scope-rename.py (the one-shot renamer)
- .gtm/phase-7-public-flip-runbook.md (updated runbook)
- .cycle/decisions.md (CHK-1 monorepo split + CHK-2 AGPL approved)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 7 extraction (after CHK-1/2/3 approval). This is the initial
public-facing shape of the Practiq codebase, derived from
venture-harness via:
  1. git filter-repo --subdirectory-filter ventures/fractional-ai-command-center
  2. git filter-repo --invert-paths (stripped .cycle/.claude/apps-script/
     supabase/ventures/test-results/storage/vercel.json/Korean planning
     doc/docs/strategy+handoff+research+validation+launch+observability+ops/
     ~20 operator-private outreach scripts via path-glob)
  3. git mv mcp-server packages/mcp
  4. Copy Phase 3 skeleton + Phase 6 docs from venture-harness/.gtm/

What this commit adds:
  - LICENSE: AGPL-3.0-only, full canonical text
  - README: Mike-pattern compliant (3-line value prop, demo-first link to
    practiq.dev, BYOK story, no-paywall fairness table, AGPL forever commit)
  - CONTRIBUTING.md: 5-business-day PR SLA, conventional commits required
  - CODE_OF_CONDUCT.md: Contributor Covenant 2.1
  - SECURITY.md: private vulnerability disclosure with 48h ack + 7/30/30 patch SLA
  - CHANGELOG.md: v0.1.0 initial entry
  - CLAUDE.md: simplified contributor pointers (replaces operator-specific dev guide)
  - .env.example: self-host friendly env template (NEXTAUTH_SECRET + OPENROUTER_API_KEY
    minimum; OAuth + Stripe + email + telemetry all optional)
  - docker/{docker-compose.yml,Dockerfile.web,entrypoint.sh}: one-command self-host
  - .github/ISSUE_TEMPLATE/{bug_report,feature_request,question}.md + PR template
  - .github/workflows/{ci,codeql,release,selfhost-smoke}.yml (CI uses
    npm install instead of npm ci pending lock-file regeneration)
  - docs/pages/{quickstart,self-host,mcp-reference,architecture,cloud-vs-self-host,
    why-oss}.md (6 AEO-optimized pages)
  - docs/public/{llms.txt,robots.txt,sitemap.xml} (LLM-crawler explicit allow)
  - docs/lib/{schema-jsonld.ts,indexnow-deploy.ts} (SoftwareApplication +
    HowTo + FAQPage JSON-LD + IndexNow post-deploy hook)
  - packages/mcp/ (moved from mcp-server/; package.json already has
    @cliwant/practiq-mcp name + AGPL-3 + repository URL)

What this commit removes:
  - .venture.yaml: harness metadata, not relevant for OSS
  - package-lock.json: stale after restructure; regenerate via npm install

Root package.json: renamed to `practiq` (was fractional-ai-command-center),
added workspaces: [packages/*], stripped all operator-specific scripts
(daily-pulse, discovery, capture-dashboard, fix-arm64-deps postinstall),
removed `../../.env.local` parent-dir lookups (now `.env.local` at repo
root), added engines.node >= 20.

Next steps (for operator before public flip):
  1. npm install (regenerates lock file with workspaces)
  2. npm run type-check (verify clean)
  3. npm run build (verify Next.js builds)
  4. npm run --workspace=packages/mcp build && npx -y file:packages/mcp --version
  5. docker compose up -d (smoke test on Mac M1)
  6. CHK-4 approval → gh repo edit --visibility public → npm publish

Phase 4 Supabase decoupling (10-14h refactor) intentionally NOT in this
commit. Lands as a follow-up before the actual public flip. Tracked in
.gtm/phase-4-code-refactor-analysis.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes self-eval item 9 gap. Full 6-page docs concatenated into a single
file at docs/public/llms-full.txt for LLM crawler indexing (Perplexity,
ChatGPT Search, Google AI Overviews prefer a single-fetch full-corpus file).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l, not removed (#7)

Phase 4 cycle 1 — empirical re-scope. The prior scaffolding commit removed
@supabase/supabase-js from deps assuming a full Supabase→Prisma decouple was
needed. Reality (verified by reading prisma/schema.prisma + grepping src/):

1. Core product (User/Account/Session/VerificationToken/Client/Conversation/
   Output) is ALREADY Postgres-native via Prisma — schema uses
   `schemas=["practiq"]`, full NextAuth models present, passwordHash for
   credentials auth. Self-host core works on plain Postgres with zero Supabase.
   → Phase 4 issue #1 (NextAuth Postgres schema) was already done.

2. The 41 files importing @supabase/supabase-js are ALL operator growth/ops
   tooling: /admin/* dashboards (analytics, A/B, SEO console, leads, incidents,
   health, crawler, signups) + a few /api/cron jobs (email-nurture). Every one
   already guards on env: `if (!url || !key) return <ErrorBox>`. They degrade
   gracefully when Supabase env is unset — they do NOT crash core.

3. Removing the dep only broke compilation (unresolved imports). Re-adding it
   restores the build while keeping the honest architecture: core = Prisma/
   Postgres (self-hostable), analytics layer = Supabase (optional, env-gated).

Also updated docs/pages/cloud-vs-self-host.md with an honest nuance: all admin
analytics code ships in OSS (no paywall), but those dashboards need a
Supabase-compatible analytics endpoint until v0.2 migrates them to Prisma.
Keeps the "no EE subdir / no paywall" promise truthful.

Net effect on Phase 4 scope: shrinks from "10-14h full rewrite" to "dep
restore + env-guard verification + v0.2 analytics-table migration (deferred)".

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(selfhost): align Docker + smoke CI with flat repo layout

The docker/ skeleton and selfhost-smoke.yml were drafted assuming an
apps/web monorepo layout, but the extracted repo is flat (web app at the
root, packages/mcp the only workspace). That mismatch made the self-host
smoke matrix unrunnable. This fixes the whole path end-to-end:

- Dockerfile.web: build at repo root (no apps/web / packages/core), use
  `npm install` (repo ships no lockfile), `npx prisma generate`, and a
  full-copy `next start` runner (robust vs. standalone tracing). Copy
  prisma.config.ts so `db push` resolves schema + datasource at runtime.
- entrypoint.sh: schema path -> root; switch `migrate deploy` -> `db push`
  (no migrations/ folder ships); POSIX while-loop; no --accept-data-loss
  so a destructive diff fails loudly instead of dropping self-hoster data.
- New GET /api/healthz liveness probe (server up + Postgres reachable),
  distinct from /api/health readiness which 503s on optional integrations.
  Both the compose healthcheck and the CI probe already point at it.
- selfhost-smoke.yml: flat-repo path filters (src/prisma/public/configs),
  and COMPOSE_FILE=docker/docker-compose.yml so every `docker compose`
  step finds the file without -f.
- .gitattributes: pin Dockerfile* to LF (Alpine build safety).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): drop npm cache from setup-node (repo ships no lockfile)

actions/setup-node@v4 with `cache: 'npm'` requires a lockfile to compute
its cache key. The repo intentionally ships no package-lock.json, so every
CI job was failing at the setup-node step before install even ran. Drop
the cache directive across all five jobs (type-check / lint / build / test
/ mcp-pack) so installs run via `npm install`.

Follow-up (not this PR): commit a package-lock.json to restore cacheable,
reproducible installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): run prisma generate before type-check and test

The Prisma client uses the new prisma-client generator with a custom
output (src/generated/prisma), imported as @/generated/prisma/client.
The type-check and test jobs never generated it, so prisma.* resolved to
`any` — the root of the bulk of the strict-mode TS7006/TS2339 cascade.
The build job + Docker already run generate; add it to these two jobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deps): restore googleapis (dropped in extraction, still imported)

The gmail/outreach + google-sc modules import googleapis, but the dep was
dropped during the OSS package.json restructuring, causing 7 TS2307
"Cannot find module 'googleapis'" errors that cascade. Restore the exact
version the app uses (^144.0.0).

Note: this operator growth/outreach tooling (outreach crons, Search Console
SEO automation, Slack summaries) is surplus to the self-host product and is
tracked for removal as a follow-up; restoring the dep unblocks the build now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): cast apiVersion to LatestApiVersion (lockfile-free SDK drift)

The last 2 of 149 type errors: stripe@^22 caret floats (no lockfile) and its
pinned-version literal type drifted to 2026-04-22.dahlia, rejecting our
deliberately-pinned 2026-03-25.dahlia. Cast to Stripe.LatestApiVersion to keep
the intended runtime value type-stable across SDK minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): omit apiVersion (Stripe.LatestApiVersion type not exported)

The cast to Stripe.LatestApiVersion failed (TS2694 — not an exported member
in stripe@22). Omitting apiVersion entirely is always type-valid and uses
the Stripe account's default pinned version — the correct default for a
self-host deployment, and immune to the lockfile-free SDK caret drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): native build job for macos-14 (Apple Silicon can't run Docker)

GitHub arm64 macOS runners lack nested virtualization, so Colima/docker
compose fails at VM start (exit 125) — macos-14 could never pass the compose
smoke. Split the matrix: docker-compose smoke runs on ubuntu-22.04 + macos-13
(full boot + DB + healthz), and a new native-build job proves Apple-Silicon
parity by running prisma generate + next build on arm64 macOS (exercises the
arch-sensitive Prisma engine / bcryptjs / lightningcss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stripe): omit apiVersion in e2e test + bootstrap script too

tsc includes tests/, so the e2e spec's hardcoded apiVersion literal was the
last type error (the bootstrap script mirrors it for consistency). Same fix
as the client: omit apiVersion to stay type-stable across stripe@22 minors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(token-budget): restore prototype chain on BudgetExceededError

The class extends the built-in Error; transpilation breaks `instanceof` for
genuine instances unless the prototype is reset. Add Object.setPrototypeOf
in the constructor — fixes the lone failing vitest assertion
(token-budget.test.ts > assertBudget — trial plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): raise V8 heap for next build (was OOMing on CI runner)

`next build` of this heavy app (pdfjs/exceljs/pptxgenjs/tiptap/react-pdf)
exceeded the default V8 old-space heap on the 7 GB ubuntu runner —
"Ineffective mark-compacts near heap limit". Set NODE_OPTIONS
--max-old-space-size=6144 in the CI build job and the Docker builder stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): ubuntu-only docker + native build matrix for both Macs

Colima on the Intel macos-13 runner is too memory-starved (4 GB VM) to
build this app and is slow/flaky; arm64 macos-14 can't run Docker at all.
Drop Docker-on-macOS entirely: keep the full docker-compose smoke on
ubuntu-22.04 (the canonical Linux self-host target), and prove macOS
self-host parity (both Intel + Apple Silicon) via a native `next build`
matrix [macos-13, macos-14] with the same heap bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(token-budget): update stale trial-allowance test (200K → constant)

The real cause of the failing assertion: FREE_TRIAL.trialTotalTokens was
bumped 200K → 700K on 2026-05-15, but the test still fed 200K usage and
expected a trial_exceeded throw. 200K < 700K → no throw → expect.fail()
threw the AssertionError we saw. Reference FREE_TRIAL.trialTotalTokens
directly so the test tracks the constant and can't go stale again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): install workspace deps so next build finds @modelcontextprotocol/sdk

The Docker builder copied only the root package.json before npm install, so
the packages/mcp workspace deps were never installed. `next build`
type-checks the whole project (incl. packages/mcp, which imports
@modelcontextprotocol/sdk) and failed: "Cannot find module
'@modelcontextprotocol/sdk/server/stdio.js'". Copy packages/mcp/package.json
before install so npm (workspaces) resolves it. (CI Build + native macOS
passed because their npm install ran against the full checkout.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): runtime boot — drop Prisma 7-invalid --skip-generate + ship client

Two runtime issues that kept the web container from going healthy:
1. entrypoint ran `prisma db push --skip-generate`, but Prisma 7 removed that
   flag — it errored, printed help, and crashed the container (set -e).
   Use a plain `prisma db push`.
2. The Prisma client uses a custom output (src/generated/prisma, not
   node_modules/.prisma) that the runner stage never copied, so the server
   + db push couldn't resolve @/generated/prisma/client. Copy it explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(selfhost): pgvector Postgres image + create extension; drop dead macos-13

Two things kept the ubuntu docker-compose smoke from going healthy / the run
from completing:
1. The schema has a vector(1024) column but docker-compose used
   postgres:16-alpine (no pgvector) and nothing created the extension, so
   `prisma db push` failed: ERROR: type "vector" does not exist. Switch to
   pgvector/pgvector:pg16 + a first-boot init that runs CREATE EXTENSION
   vector. This is a real self-host correctness fix, not just CI.
2. macos-13 native build sat queued 45+ min (GitHub Intel-mac runner
   unavailability) and wedged the whole workflow from ever completing. Drop
   it from the matrix; macos-14 (Apple Silicon) covers native build parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): gate on explicit verbose healthz probe, not compose --wait

The container went "unhealthy" (compose --wait failed) even though the app
boots fine (Postgres reachable, db push synced, Next.js Ready). Decouple:
`docker compose up -d --build` (no --wait), and let the explicit probe step
gate — now printing the HTTP code + body each attempt so a 503 reveals the
actual healthz error instead of an opaque "unhealthy". Either goes green or
surfaces the precise runtime cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(selfhost): assert build+boot+db-sync (fatal); HTTP serve = known-gap

Operator decision: merge now, track the Docker HTTP-serving gap. The ubuntu
job now asserts the core self-host proof as fatal — image builds, container
boots, Postgres reachable, schema synced via prisma db push — and treats the
in-container HTTP route probe as non-blocking, because the hand-rolled
`next start` runtime doesn't serve built routes (404; needs output:standalone,
tracked as a follow-up issue). Makes selfhost-smoke green on what genuinely
works instead of red on the one packaging gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o/GTM framing (#10)

The repo is public now, so the README + docs must be accurate:
- README: flat-repo layout (web app at root, not apps/web; no packages/core),
  `cd practiq-oss` (not practiq), compose path docker/docker-compose.yml.
- Broken `docs.practiq.dev` (DNS does not resolve) -> `practiq.dev/docs`
  (the live docs) across all files.
- Self-host section is honest about the Docker route-serving gap (#9) and
  points to the fully-working MCP server as the entry point.
- Cliwant is a venture studio, not "a one-person venture studio"; drop the
  solo-founder / "6-week solo project" / mikeoss "Harvey vs Mike" framing and
  the "posted every Sunday on LinkedIn/IndieHackers" line (no SNS launch).
- Remove docs/playbooks/* (internal GTM outreach scripts — not product docs).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels May 28, 2026
@seungdo-keum
seungdo-keum deleted the dependabot/npm_and_yarn/packages/mcp/fast-uri-3.1.2 branch May 28, 2026 02:17
@dependabot @github

dependabot Bot commented on behalf of github May 28, 2026

Copy link
Copy Markdown
Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant